Browse Source

Nc fix/shared view UI changes (#8615)

* fix(nc-gui): update shared grid view

* fix(nc-gui): shared gallery view padding issue

* fix(nc-gui): Shared kanban view padding issue

* fix(nc-gui): reduce calender shared view padding

* fix(nc-gui): reduce shared form view padding

* fix(nc-gui): update shared view password modal

* fix(nc-gui): shared view password input error handling

* fix(nc-gui): reduce expanded form modal width if comment section is not present

* fix(nc-gui): small changes

* fix(nc-gui): add export download view in topbar of shared view

* fix(nc-gui): small changes

* fix(nc-gui): add blur bg image for shared view password modal

* fix(nc-gui): download shared view dropdown ui changes

* fix(nc-gui): expanded form scroll issue

* fix(nc-gui): click anywhere in card should open expanded form

* fix(nc-gui): hide action icon on gallery/kanban card hover

* fix(nc-gui): expanded form cell hover effect

* fix(nc-gui): add sign up for free btn in shared view

* test: update shared view test cases

* test: update calendar test cases

* fix(nc-gui): remove readonly prefix from attachment modal

* fix(nc-gui): remove focus border effect if field is readonly

* fix(nc-gui): shared view groupby pagination size should be 10

* fix(nc-gui): remove field modal input shadow if field is disabled

* fix(nc-gui): add shadow on expanded form fields

* fix(nc-gui): calendar shared view background color update

* fix(nc-gui): shared view download btn text color

* fix(nc-gui): update url, link, email grid text color if cell is active and remove hover effect

* fix(nc-gui): pr review changes
pull/8640/head
Ramesh Mane 6 months ago committed by GitHub
parent
commit
575ff920ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. BIN
      packages/nc-gui/assets/img/views/calendar.png
  2. BIN
      packages/nc-gui/assets/img/views/form.png
  3. BIN
      packages/nc-gui/assets/img/views/gallery.png
  4. BIN
      packages/nc-gui/assets/img/views/grid.png
  5. BIN
      packages/nc-gui/assets/img/views/kanban.png
  6. 5
      packages/nc-gui/assets/nc-icons/key.svg
  7. 2
      packages/nc-gui/components/cell/DatePicker.vue
  8. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  9. 2
      packages/nc-gui/components/cell/Email.vue
  10. 4
      packages/nc-gui/components/cell/Json.vue
  11. 2
      packages/nc-gui/components/cell/PhoneNumber.vue
  12. 2
      packages/nc-gui/components/cell/TimePicker.vue
  13. 4
      packages/nc-gui/components/cell/Url.vue
  14. 2
      packages/nc-gui/components/cell/YearPicker.vue
  15. 1
      packages/nc-gui/components/cell/attachment/Modal.vue
  16. 19
      packages/nc-gui/components/nc/Button.vue
  17. 94
      packages/nc-gui/components/shared-view/AskPassword.vue
  18. 4
      packages/nc-gui/components/shared-view/Calendar.vue
  19. 2
      packages/nc-gui/components/shared-view/Gallery.vue
  20. 2
      packages/nc-gui/components/shared-view/Grid.vue
  21. 2
      packages/nc-gui/components/shared-view/Kanban.vue
  22. 2
      packages/nc-gui/components/smartsheet/Cell.vue
  23. 6
      packages/nc-gui/components/smartsheet/Gallery.vue
  24. 8
      packages/nc-gui/components/smartsheet/Kanban.vue
  25. 2
      packages/nc-gui/components/smartsheet/Toolbar.vue
  26. 11
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  27. 15
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  28. 45
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  29. 20
      packages/nc-gui/components/smartsheet/grid/Table.vue
  30. 2
      packages/nc-gui/components/smartsheet/header/Cell.vue
  31. 2
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  32. 48
      packages/nc-gui/components/smartsheet/toolbar/Export.vue
  33. 1
      packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue
  34. 2
      packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue
  35. 6
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  36. 6
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  37. 37
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  38. 29
      packages/nc-gui/composables/useSharedView.ts
  39. 2
      packages/nc-gui/composables/useViewGroupBy.ts
  40. 3
      packages/nc-gui/lang/en.json
  41. 77
      packages/nc-gui/layouts/shared-view.vue
  42. 2
      packages/nc-gui/lib/types.ts
  43. 3
      packages/nc-gui/pages/index/[typeOrId]/calendar/[viewId]/index.vue
  44. 74
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId].vue
  45. 2
      packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue
  46. 3
      packages/nc-gui/pages/index/[typeOrId]/gallery/[viewId]/index.vue
  47. 3
      packages/nc-gui/pages/index/[typeOrId]/kanban/[viewId]/index.vue
  48. 3
      packages/nc-gui/pages/index/[typeOrId]/map/[viewId]/index.vue
  49. 4
      packages/nc-gui/pages/index/[typeOrId]/view/[viewId].vue
  50. 2
      packages/nc-gui/utils/iconUtils.ts
  51. 14
      tests/playwright/pages/Dashboard/common/Toolbar/CalendarViewMode.ts
  52. 22
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  53. 26
      tests/playwright/pages/Dashboard/common/Topbar/index.ts
  54. 6
      tests/playwright/tests/db/views/viewGridShare.spec.ts

BIN
packages/nc-gui/assets/img/views/calendar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

BIN
packages/nc-gui/assets/img/views/form.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
packages/nc-gui/assets/img/views/gallery.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
packages/nc-gui/assets/img/views/grid.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

BIN
packages/nc-gui/assets/img/views/kanban.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

5
packages/nc-gui/assets/nc-icons/key.svg

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M10.3333 4.99998L12.6667 2.66665M14 1.33331L12.6667 2.66665L14 1.33331ZM7.59333 7.73998C7.93756 8.07962 8.2112 8.48401 8.3985 8.92984C8.5858 9.37568 8.68306 9.85416 8.68468 10.3377C8.68631 10.8213 8.59225 11.3004 8.40794 11.7475C8.22363 12.1946 7.95271 12.6008 7.61076 12.9427C7.26882 13.2847 6.86261 13.5556 6.41554 13.7399C5.96846 13.9242 5.48933 14.0183 5.00575 14.0167C4.52218 14.015 4.0437 13.9178 3.59786 13.7305C3.15203 13.5432 2.74764 13.2695 2.408 12.9253C1.74009 12.2338 1.37051 11.3076 1.37886 10.3462C1.38722 9.38479 1.77284 8.46514 2.45267 7.78531C3.13249 7.10548 4.05214 6.71986 5.01353 6.71151C5.97492 6.70315 6.90113 7.07273 7.59267 7.74065L7.59333 7.73998ZM7.59333 7.73998L10.3333 4.99998L7.59333 7.73998ZM10.3333 4.99998L12.3333 6.99998L14.6667 4.66665L12.6667 2.66665L10.3333 4.99998Z"
stroke="currentColor" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

2
packages/nc-gui/components/cell/DatePicker.vue

@ -321,7 +321,7 @@ function handleSelectDate(value?: dayjs.Dayjs) {
<GeneralIcon <GeneralIcon
v-if="localState && !readOnly" v-if="localState && !readOnly"
icon="closeCircle" icon="closeCircle"
class="nc-clear-date-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer" class="nc-clear-date-icon nc-action-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()" @click.stop="handleSelectDate()"
/> />
</div> </div>

2
packages/nc-gui/components/cell/DateTimePicker.vue

@ -525,7 +525,7 @@ const cellValue = computed(
<GeneralIcon <GeneralIcon
v-if="localState && (isExpandedForm || isForm || !isGrid || isEditColumn) && !readOnly" v-if="localState && (isExpandedForm || isForm || !isGrid || isEditColumn) && !readOnly"
icon="closeCircle" icon="closeCircle"
class="nc-clear-date-time-icon h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer" class="nc-clear-date-time-icon nc-action-icon h-4 w-4 absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()" @click.stop="handleSelectDate()"
/> />
</div> </div>

2
packages/nc-gui/components/cell/Email.vue

@ -97,7 +97,7 @@ watch(
<nuxt-link <nuxt-link
v-else-if="validEmail" v-else-if="validEmail"
no-ref no-ref
class="py-1 underline hover:opacity-75 inline-block nc-cell-field-link max-w-full" class="py-1 underline inline-block nc-cell-field-link max-w-full"
:href="`mailto:${vModel}`" :href="`mailto:${vModel}`"
target="_blank" target="_blank"
:tabindex="readOnly ? -1 : 0" :tabindex="readOnly ? -1 : 0"

4
packages/nc-gui/components/cell/Json.vue

@ -181,11 +181,11 @@ watch(inputWrapperRef, () => {
> >
<a-button <a-button
:type="isEditColumn && !isExpanded ? 'text' : 'primary'" :type="!isExpanded ? 'text' : 'primary'"
size="small" size="small"
class="nc-save-json-value-btn !rounded-lg" class="nc-save-json-value-btn !rounded-lg"
:class="{ :class="{
'nc-edit-modal': isEditColumn && !isExpanded, 'nc-edit-modal': !isExpanded,
}" }"
:disabled="!!error || localValue === vModel" :disabled="!!error || localValue === vModel"
@click="onSave" @click="onSave"

2
packages/nc-gui/components/cell/PhoneNumber.vue

@ -79,7 +79,7 @@ watch(
<a <a
v-else-if="validPhoneNumber" v-else-if="validPhoneNumber"
class="py-1 underline hover:opacity-75 inline-block nc-cell-field-link" class="py-1 underline inline-block nc-cell-field-link"
:href="`tel:${vModel}`" :href="`tel:${vModel}`"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"

2
packages/nc-gui/components/cell/TimePicker.vue

@ -331,7 +331,7 @@ const cellValue = computed(() => localState.value?.format(parseProp(column.value
<GeneralIcon <GeneralIcon
v-if="localState && !readOnly" v-if="localState && !readOnly"
icon="closeCircle" icon="closeCircle"
class="nc-clear-time-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer" class="nc-clear-time-icon nc-action-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectTime()" @click.stop="handleSelectTime()"
/> />
</div> </div>

4
packages/nc-gui/components/cell/Url.vue

@ -102,7 +102,7 @@ watch(
v-else-if="isValid && !cellUrlOptions?.overlay" v-else-if="isValid && !cellUrlOptions?.overlay"
no-prefetch no-prefetch
no-rel no-rel
class="py-1 z-3 underline hover:opacity-75 nc-cell-field-link max-w-full" class="py-1 z-3 underline nc-cell-field-link max-w-full"
:to="url" :to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'" :target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
:tabindex="readOnly ? -1 : 0" :tabindex="readOnly ? -1 : 0"
@ -114,7 +114,7 @@ watch(
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay" v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
no-prefetch no-prefetch
no-rel no-rel
class="py-1 z-3 w-full h-full text-center !no-underline hover:opacity-75 nc-cell-field-link max-w-full" class="py-1 z-3 w-full h-full text-center !no-underline nc-cell-field-link max-w-full"
:to="url" :to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'" :target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
:tabindex="readOnly ? -1 : 0" :tabindex="readOnly ? -1 : 0"

2
packages/nc-gui/components/cell/YearPicker.vue

@ -283,7 +283,7 @@ function handleSelectDate(value?: dayjs.Dayjs) {
<GeneralIcon <GeneralIcon
v-if="localState && !readOnly" v-if="localState && !readOnly"
icon="closeCircle" icon="closeCircle"
class="nc-clear-year-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer" class="nc-clear-year-icon nc-action-icon absolute right-0 top-[50%] transform -translate-y-1/2 invisible cursor-pointer"
@click.stop="handleSelectDate()" @click.stop="handleSelectDate()"
/> />
</div> </div>

1
packages/nc-gui/components/cell/attachment/Modal.vue

@ -103,7 +103,6 @@ const handleFileDelete = (i: number) => {
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div v-if="readOnly" class="text-gray-400">[{{ $t('labels.readOnly') }}]</div>
{{ $t('labels.viewingAttachmentsOf') }} {{ $t('labels.viewingAttachmentsOf') }}
<div class="font-semibold underline">{{ column?.title }}</div> <div class="font-semibold underline">{{ column?.title }}</div>
</div> </div>

19
packages/nc-gui/components/nc/Button.vue

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ButtonType } from 'ant-design-vue/lib/button' import type { ButtonType } from 'ant-design-vue/lib/button'
import { useSlots } from 'vue' import { useSlots } from 'vue'
import type { NcButtonSize } from '~/lib/types'
/** /**
* @description * @description
@ -76,11 +75,12 @@ useEventListener(NcButton, 'mousedown', () => {
<a-button <a-button
ref="NcButton" ref="NcButton"
:class="{ :class="{
small: size === 'small', 'small': size === 'small',
medium: size === 'medium', 'medium': size === 'medium',
xsmall: size === 'xsmall', 'xsmall': size === 'xsmall',
xxsmall: size === 'xxsmall', 'xxsmall': size === 'xxsmall',
focused: isFocused, 'size-xs': size === 'xs',
'focused': isFocused,
}" }"
:disabled="props.disabled" :disabled="props.disabled"
:loading="loading" :loading="loading"
@ -168,6 +168,13 @@ useEventListener(NcButton, 'mousedown', () => {
@apply py-2 px-4 h-10 min-w-10 xs:(h-10.5 max-h-10.5 min-w-10.5 !px-3); @apply py-2 px-4 h-10 min-w-10 xs:(h-10.5 max-h-10.5 min-w-10.5 !px-3);
} }
.nc-button.ant-btn.size-xs {
@apply px-2 py-0 h-7 min-w-7 rounded-lg text-small leading-[18px];
& > div {
@apply gap-x-2;
}
}
.nc-button.ant-btn.xsmall { .nc-button.ant-btn.xsmall {
@apply p-0.25 h-6.25 min-w-6.25 rounded-md; @apply p-0.25 h-6.25 min-w-6.25 rounded-md;
} }

94
packages/nc-gui/components/shared-view/AskPassword.vue

@ -1,9 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core' import type { VNodeRef } from '@vue/runtime-core'
import type { InputPassword } from 'ant-design-vue' import type { InputPassword } from 'ant-design-vue'
import { ViewTypes } from 'nocodb-sdk'
import gridImage from '~/assets/img/views/grid.png'
import galleryImage from '~/assets/img/views/gallery.png'
import kanbanImage from '~/assets/img/views/kanban.png'
import calendarImage from '~/assets/img/views/calendar.png'
const props = defineProps<{ const props = defineProps<{
modelValue: boolean modelValue: boolean
viewType?: ViewTypes
}>() }>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -16,47 +22,103 @@ const { loadSharedView } = useSharedView()
const formState = ref({ password: undefined }) const formState = ref({ password: undefined })
const passwordError = ref<string | null>(null)
const onFinish = async () => { const onFinish = async () => {
try { try {
await loadSharedView(route.params.viewId as string, formState.value.password) await loadSharedView(route.params.viewId as string, formState.value.password)
vModel.value = false vModel.value = false
} catch (e: any) { } catch (e: any) {
console.error(e) const error = await extractSdkResponseErrorMsgv2(e)
message.error(await extractSdkResponseErrorMsg(e)) console.error(error.message)
if (error.error === NcErrorType.INVALID_SHARED_VIEW_PASSWORD) {
passwordError.value = error.message
} else {
message.error(error.message)
}
} }
} }
const focus: VNodeRef = (el: typeof InputPassword) => el?.$el?.querySelector('input').focus() const focus: VNodeRef = (el: typeof InputPassword) => {
return el && el?.focus?.()
}
watch(
() => formState.value.password,
() => {
passwordError.value = null
},
)
const bgImageName = computed(() => {
switch (props.viewType) {
case ViewTypes.GRID:
return gridImage
case ViewTypes.GALLERY:
return galleryImage
case ViewTypes.KANBAN:
return kanbanImage
case ViewTypes.CALENDAR:
return calendarImage
default:
return gridImage
}
})
</script> </script>
<template> <template>
<NcModal v-model:visible="vModel" c size="small" :class="{ active: vModel }" :mask-closable="false"> <NcModal
<template #header> v-model:visible="vModel"
<div class="flex flex-row items-center gap-x-2"> c
<GeneralIcon icon="key" /> size="small"
:class="{ active: vModel }"
:mask-closable="false"
:mask-style="{
backgroundColor: 'rgba(255, 255, 255, 0.64)',
backdropFilter: 'blur(8px)',
}"
>
<div class="flex flex-col gap-5">
<div class="flex flex-row items-center gap-x-2 text-base font-weight-700 text-gray-800">
<GeneralIcon icon="ncKey" class="!text-base w-5 h-5" />
{{ $t('msg.thisSharedViewIsProtected') }} {{ $t('msg.thisSharedViewIsProtected') }}
</div> </div>
</template>
<div class="mt-2">
<a-form ref="formRef" :model="formState" name="create-new-table-form" @finish="onFinish"> <a-form ref="formRef" :model="formState" name="create-new-table-form" @finish="onFinish">
<a-form-item name="password" :rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]"> <a-form-item
name="password"
:rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]"
class="!mb-0"
>
<a-input-password <a-input-password
ref="focus" :ref="focus"
v-model:value="formState.password" v-model:value="formState.password"
class="nc-input-md" class="!rounded-lg !text-small"
hide-details hide-details
size="large"
:placeholder="$t('msg.enterPassword')" :placeholder="$t('msg.enterPassword')"
/> />
<Transition name="layout">
<div v-if="passwordError" class="mb-2 text-sm text-red-500">{{ passwordError }}</div>
</Transition>
</a-form-item> </a-form-item>
</a-form> </a-form>
<div class="flex flex-row justify-end gap-x-2 mt-6"> <div class="flex flex-row justify-end gap-x-2">
<NcButton type="primary" html-type="submit" @click="onFinish" <NcButton
>{{ $t('general.unlock') }} :disabled="!formState.password"
type="primary"
size="small"
html-type="submit"
class="!px-2"
data-testid="nc-shared-view-password-submit-btn"
@click="onFinish"
>
{{ $t('objects.view') }}
<template #loading> {{ $t('msg.verifyingPassword') }}</template> <template #loading> {{ $t('msg.verifyingPassword') }}</template>
</NcButton> </NcButton>
</div> </div>
</div> </div>
</NcModal> </NcModal>
<img alt="view image" :src="bgImageName" class="fixed inset-0 w-full h-full" />
</template> </template>

4
packages/nc-gui/components/shared-view/Calendar.vue

@ -25,10 +25,10 @@ useProvideCalendarViewStore(meta, sharedView, true, nestedFilters)
</script> </script>
<template> <template>
<div class="nc-container h-full mt-1.5 px-12"> <div class="nc-container h-full">
<div class="flex flex-col h-full flex-1 min-w-0"> <div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar /> <LazySmartsheetToolbar />
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50"> <div class="h-full flex-1 min-w-0 min-h-0">
<LazySmartsheetCalendar /> <LazySmartsheetCalendar />
</div> </div>
</div> </div>

2
packages/nc-gui/components/shared-view/Gallery.vue

@ -23,7 +23,7 @@ useProvideKanbanViewStore(meta, sharedView)
</script> </script>
<template> <template>
<div class="nc-container h-full mt-1.5 px-12"> <div class="nc-container h-full">
<div class="flex flex-col h-full flex-1 min-w-0"> <div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar /> <LazySmartsheetToolbar />
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50"> <div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">

2
packages/nc-gui/components/shared-view/Grid.vue

@ -45,7 +45,7 @@ watch(
</script> </script>
<template> <template>
<div class="nc-container flex flex-col h-full mt-1.5 px-12"> <div class="nc-container flex flex-col h-full">
<LazySmartsheetToolbar /> <LazySmartsheetToolbar />
<LazySmartsheetGrid /> <LazySmartsheetGrid />
</div> </div>

2
packages/nc-gui/components/shared-view/Kanban.vue

@ -23,7 +23,7 @@ useProvideKanbanViewStore(meta, sharedView, true)
</script> </script>
<template> <template>
<div class="nc-container h-full mt-1.5 px-12"> <div class="nc-container h-full">
<div class="flex flex-col h-full flex-1 min-w-0"> <div class="flex flex-col h-full flex-1 min-w-0">
<LazySmartsheetToolbar /> <LazySmartsheetToolbar />
<div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50"> <div class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">

2
packages/nc-gui/components/smartsheet/Cell.vue

@ -186,7 +186,7 @@ const onContextmenu = (e: MouseEvent) => {
<LazyCellJson v-else-if="isJSON(column)" v-model="vModel" /> <LazyCellJson v-else-if="isJSON(column)" v-model="vModel" />
<LazyCellText v-else v-model="vModel" /> <LazyCellText v-else v-model="vModel" />
<div <div
v-if="((isPublic && readOnly && !isForm) || (isSystemColumn(column) && !isAttachment(column))) && !isTextArea(column)" v-if="((isPublic && readOnly && !isForm) || isSystemColumn(column)) && !isAttachment(column) && !isTextArea(column)"
class="nc-locked-overlay" class="nc-locked-overlay"
/> />
</template> </template>

6
packages/nc-gui/components/smartsheet/Gallery.vue

@ -396,4 +396,10 @@ watch(
.ant-carousel.gallery-carousel :deep(.slick-next) { .ant-carousel.gallery-carousel :deep(.slick-next) {
@apply right-0; @apply right-0;
} }
:deep(.ant-card) {
&:hover .nc-action-icon {
@apply invisible;
}
}
</style> </style>

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

@ -512,7 +512,7 @@ const getRowId = (row: RowType) => {
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<a-card <a-card
:key="`${getRowId(record)}-${index}`" :key="`${getRowId(record)}-${index}`"
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer" class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer children:pointer-events-none"
:body-style="{ padding: '0px' }" :body-style="{ padding: '0px' }"
:data-stack="stack.title" :data-stack="stack.title"
:data-testid="`nc-gallery-card-${record.row.id}`" :data-testid="`nc-gallery-card-${record.row.id}`"
@ -809,4 +809,10 @@ const getRowId = (row: RowType) => {
:deep(.slick-slide) { :deep(.slick-slide) {
@apply !pointer-events-none; @apply !pointer-events-none;
} }
:deep(.ant-card) {
&:hover .nc-action-icon {
@apply invisible;
}
}
</style> </style>

2
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -65,8 +65,6 @@ const { allowCSVDownload } = useSharedView()
<!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> --> <!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> -->
<LazySmartsheetToolbarExport v-if="isPublic && allowCSVDownload" />
<div class="flex-1" /> <div class="flex-1" />
</template> </template>

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

@ -40,10 +40,13 @@ function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
<template> <template>
<div <div
class="nc-virtual-cell w-full flex items-center" class="nc-virtual-cell w-full flex items-center"
:class="{ :class="[
'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm, `nc-virtual-cell-${(column.uidt || 'default').toLowerCase()}`,
'nc-display-value-cell': isPrimary(column) && !isForm, {
}" 'text-right justify-end': isGrid && !isForm && isRollup(column) && !isExpandedForm,
'nc-display-value-cell': isPrimary(column) && !isForm,
},
]"
@keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="onNavigate(NavigateDir.NEXT, $event)"
@keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)" @keydown.shift.enter.exact="onNavigate(NavigateDir.PREV, $event)"
> >

15
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -557,9 +557,13 @@ const submitBtnLabel = computed(() => {
.ant-radio-wrapper-disabled { .ant-radio-wrapper-disabled {
@apply pointer-events-none; @apply pointer-events-none;
box-shadow: none;
&:hover {
box-shadow: none;
}
} }
&:not(:hover):not(:focus-within):not(.shadow-selected), &:not(.ant-radio-wrapper-disabled):not(:hover):not(:focus-within):not(.shadow-selected) {
&.ant-radio-wrapper-disabled:hover {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08); box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
} }
&:hover:not(:focus-within):not(.ant-radio-wrapper-disabled) { &:hover:not(:focus-within):not(.ant-radio-wrapper-disabled) {
@ -568,14 +572,17 @@ const submitBtnLabel = computed(() => {
} }
:deep(.ant-select) { :deep(.ant-select) {
&:not(:hover):not(.ant-select-focused) .ant-select-selector, &:not(.ant-select-disabled):not(:hover):not(.ant-select-focused) .ant-select-selector,
&:hover.ant-select-disabled .ant-select-selector { &:not(.ant-select-disabled):hover.ant-select-disabled .ant-select-selector {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08); box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
} }
&:hover:not(.ant-select-focused):not(.ant-select-disabled) .ant-select-selector { &:hover:not(.ant-select-focused):not(.ant-select-disabled) .ant-select-selector {
@apply border-gray-300; @apply border-gray-300;
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24); box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24);
} }
&.ant-select-disabled .ant-select-selector {
box-shadow: none;
}
} }
:deep(.ant-form-item-label > label) { :deep(.ant-form-item-label > label) {

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

@ -547,13 +547,13 @@ export default {
:closable="false" :closable="false"
:footer="null" :footer="null"
:visible="isExpanded" :visible="isExpanded"
:width="commentsDrawer && isUIAllowed('commentList') ? 'min(80vw,1280px)' : 'min(80vw,1280px)'" :width="showRightSections ? 'min(80vw,1280px)' : 'min(70vw,768px)'"
class="nc-drawer-expanded-form" class="nc-drawer-expanded-form"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
v-bind="modalProps" v-bind="modalProps"
@update:visible="onIsExpandedUpdate" @update:visible="onIsExpandedUpdate"
> >
<div class="h-[85vh] xs:(max-h-full h-auto) max-h-215 flex flex-col"> <div class="h-[85vh] xs:(max-h-full h-full) max-h-215 flex flex-col">
<div v-if="isMobileMode" class="flex-none h-4 flex items-center justify-center"> <div v-if="isMobileMode" class="flex-none h-4 flex items-center justify-center">
<div class="flex-none h-full flex items-center justify-center cursor-pointer" @click="onClose"> <div class="flex-none h-full flex items-center justify-center cursor-pointer" @click="onClose">
<div class="w-[72px] h-[2px] rounded-full bg-[#49494a]"></div> <div class="w-[72px] h-[2px] rounded-full bg-[#49494a]"></div>
@ -720,13 +720,13 @@ export default {
</NcButton> </NcButton>
</div> </div>
</div> </div>
<div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%-4rem)] w-full border-t-1 border-gray-200"> <div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%_-_4rem)] w-full border-t-1 border-gray-200">
<div <div
:class="{ :class="{
'w-full': !showRightSections, 'w-full': !showRightSections,
'flex-1': showRightSections, 'flex-1': showRightSections,
}" }"
class="flex xs:w-full flex-col overflow-hidden" class="h-full flex xs:w-full flex-col overflow-hidden"
> >
<div <div
ref="expandedFormScrollWrapper" ref="expandedFormScrollWrapper"
@ -765,7 +765,8 @@ export default {
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white flex-1 <lg:w-full px-1 min-h-[37px] flex items-center relative" class="bg-white flex-1 <lg:w-full px-1 min-h-[37px] flex items-center relative"
:class="{ :class="{
'!bg-gray-50 !select-text nc-system-field': isReadOnlyVirtualCell(col), ' !select-text nc-system-field': isReadOnlyVirtualCell(col),
'!select-text nc-readonly-div-data-cell': readOnly,
}" }"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
@ -840,7 +841,8 @@ export default {
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="bg-white flex-1 <lg:w-full px-1 min-h-[37px] flex items-center relative" class="bg-white flex-1 <lg:w-full px-1 min-h-[37px] flex items-center relative"
:class="{ :class="{
'!bg-gray-50 !select-text nc-system-field': isReadOnlyVirtualCell(col), '!select-text nc-system-field': isReadOnlyVirtualCell(col),
'!bg-gray-50 !select-text nc-readonly-div-data-cell': readOnly,
}" }"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
@ -933,6 +935,7 @@ export default {
</NcButton> </NcButton>
</div> </div>
</div> </div>
<div v-else class="p-2"></div>
</div> </div>
<div <div
v-if="showRightSections" v-if="showRightSections"
@ -1004,6 +1007,9 @@ export default {
.nc-expanded-cell-header > :first-child { .nc-expanded-cell-header > :first-child {
@apply !text-md pl-2 xs:(pl-0 -ml-0.5); @apply !text-md pl-2 xs:(pl-0 -ml-0.5);
} }
.nc-expanded-cell-header:not(.nc-cell-expanded-form-header) > :first-child {
@apply pl-0;
}
.nc-drawer-expanded-form .nc-modal { .nc-drawer-expanded-form .nc-modal {
@apply !p-0; @apply !p-0;
@ -1022,12 +1028,31 @@ export default {
.nc-data-cell { .nc-data-cell {
@apply !rounded-lg; @apply !rounded-lg;
transition: all 0.3s; transition: all 0.3s;
&:hover {
@apply !border-1 !border-brand-400; &:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.08);
}
&:not(:focus-within):hover:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
@apply !border-1;
box-shadow: 0px 0px 4px 0px rgba(0, 0, 0, 0.24);
}
&.nc-readonly-div-data-cell,
&.nc-system-field {
@apply !border-gray-200;
.nc-cell,
.nc-virtual-cell {
@apply text-gray-400;
}
}
&.nc-readonly-div-data-cell:focus-within,
&.nc-system-field:focus-within {
@apply !border-gray-200;
} }
&:focus-within { &:focus-within:not(.nc-readonly-div-data-cell):not(.nc-system-field) {
box-shadow: 0px 0px 0px 2px rgba(51, 102, 255, 0.24) !important; @apply !shadow-selected;
} }
} }
.nc-data-cell:focus-within { .nc-data-cell:focus-within {

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

@ -2476,7 +2476,18 @@ onKeyStroke('ArrowDown', onDown)
overflow: hidden; overflow: hidden;
@apply flex h-auto; @apply flex h-auto;
} }
&.active-cell {
:deep(.nc-cell) {
a.nc-cell-field-link {
@apply !text-brand-500;
&:hover,
.nc-cell-field {
@apply !text-brand-500;
}
}
}
}
:deep(.nc-cell), :deep(.nc-cell),
:deep(.nc-virtual-cell) { :deep(.nc-virtual-cell) {
@apply !text-small; @apply !text-small;
@ -2506,6 +2517,13 @@ onKeyStroke('ArrowDown', onDown)
@apply !p-0 m-0; @apply !p-0 m-0;
} }
a.nc-cell-field-link {
@apply !text-current;
&:hover {
@apply !text-current;
}
}
&.nc-cell-longtext { &.nc-cell-longtext {
@apply leading-[18px]; @apply leading-[18px];

2
packages/nc-gui/components/smartsheet/header/Cell.vue

@ -102,7 +102,7 @@ const onClick = (e: Event) => {
'h-full': column, 'h-full': column,
'!text-gray-400': isKanban, '!text-gray-400': isKanban,
'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm, 'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm,
'cursor-pointer hover:bg-gray-100': 'nc-cell-expanded-form-header cursor-pointer hover:bg-gray-100':
isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm, isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false, 'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
}" }"

2
packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -183,7 +183,7 @@ const onClick = (e: Event) => {
:class="{ :class="{
'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm, 'flex-col !items-start justify-center pt-0.5': isExpandedForm && !isMobileMode && !isExpandedBulkUpdateForm,
'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false, 'bg-gray-100': isExpandedForm && !isExpandedBulkUpdateForm ? editColumnDropdown || isDropDownOpen : false,
'cursor-pointer hover:bg-gray-100': 'nc-cell-expanded-form-header cursor-pointer hover:bg-gray-100':
isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm, isExpandedForm && !isMobileMode && isUIAllowed('fieldEdit') && !isExpandedBulkUpdateForm,
}" }"
@dblclick="openHeaderMenu" @dblclick="openHeaderMenu"

48
packages/nc-gui/components/smartsheet/toolbar/Export.vue

@ -1,19 +1,47 @@
<script lang="ts" setup></script> <script lang="ts" setup>
const { sharedView, meta, nestedFilters } = useSharedView()
const { isLocked, xWhere } = useProvideSmartsheetStore(sharedView, meta, true, ref([]), nestedFilters)
const reloadEventHook = createEventHook()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(IsPublicInj, ref(true))
provide(IsLockedInj, isLocked)
useProvideViewColumns(sharedView, meta, () => reloadEventHook?.trigger(), true)
useProvideViewGroupBy(sharedView, meta, xWhere, true)
useProvideSmartsheetLtarHelpers(meta)
useProvideKanbanViewStore(meta, sharedView)
useProvideCalendarViewStore(meta, sharedView, true, nestedFilters)
</script>
<template> <template>
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu"> <NcDropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <NcButton v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn" size="xs" type="secondary">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center text-gray-700">
<component :is="iconMap.download" class="group-hover:text-accent text-gray-500" /> <component :is="iconMap.download" class="group-hover:text-accent" />
<span class="text-capitalize !text-sm font-medium text-gray-500">{{ $t('general.download') }}</span> <span class="text-capitalize !text-sm font-medium xs:hidden">{{ $t('general.download') }}</span>
<component :is="iconMap.arrowDown" class="text-grey" /> <component :is="iconMap.arrowDown" class="text-grey" />
</div> </div>
</a-button> </NcButton>
<template #overlay> <template #overlay>
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded"> <NcMenu class="ml-6 !text-sm !rounded-lg overflow-hidden">
<LazySmartsheetToolbarExportSubActions /> <LazySmartsheetToolbarExportSubActions />
</a-menu> </NcMenu>
</template> </template>
</a-dropdown> </NcDropdown>
</template> </template>

1
packages/nc-gui/components/smartsheet/toolbar/ExportSubActions.vue

@ -75,6 +75,7 @@ const exportFile = async (exportType: ExportTypes) => {
}, 200) }, 200)
} }
} catch (e: any) { } catch (e: any) {
isExportingType.value = undefined
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }

2
packages/nc-gui/components/smartsheet/toolbar/RowHeight.vue

@ -87,7 +87,7 @@ useMenuCloseOnEsc(open)
<NcButton <NcButton
v-e="['c:row-height']" v-e="['c:row-height']"
:disabled="isLocked" :disabled="isLocked"
class="nc-height-menu-btn nc-toolbar-btn !border-0 !h-7" class="nc-height-menu-btn nc-toolbar-btn !border-0 !h-7 !px-1.5 !min-w-7"
size="small" size="small"
type="secondary" type="secondary"
> >

6
packages/nc-gui/components/smartsheet/toolbar/SearchData.vue

@ -87,7 +87,7 @@ onClickOutside(globalSearchWrapperRef, (e) => {
<div ref="globalSearchWrapperRef" class="nc-global-search-wrapper"> <div ref="globalSearchWrapperRef" class="nc-global-search-wrapper">
<a-button <a-button
v-if="!search.query && !showSearchBox" v-if="!search.query && !showSearchBox"
class="nc-toolbar-btn" class="nc-toolbar-btn !rounded-lg !h-7 !px-1.5"
data-testid="nc-global-search-show-input" data-testid="nc-global-search-show-input"
@click="handleShowSearchInput" @click="handleShowSearchInput"
> >
@ -110,7 +110,7 @@ onClickOutside(globalSearchWrapperRef, (e) => {
> >
<GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" /> <GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div v-if="!isMobileMode" class="w-16 text-xs font-medium text-gray-400 truncate"> <div v-if="!isMobileMode" class="w-16 text-xs font-medium text-gray-400 truncate">
{{ displayColumnLabel }} {{ displayColumnLabel ?? '' }}
</div> </div>
<div class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)"> <div class="xs:(text-gray-600) group-hover:text-gray-700 sm:(text-gray-400)">
<component :is="iconMap.arrowDown" class="text-sm text-inherit" /> <component :is="iconMap.arrowDown" class="text-sm text-inherit" />
@ -136,7 +136,7 @@ onClickOutside(globalSearchWrapperRef, (e) => {
name="globalSearchQuery" name="globalSearchQuery"
size="small" size="small"
class="text-xs w-40 h-full" class="text-xs w-40 h-full"
:placeholder="`${$t('general.searchIn')} ${displayColumnLabel}`" :placeholder="`${$t('general.searchIn')} ${displayColumnLabel ?? ''}`"
:bordered="false" :bordered="false"
data-testid="search-data-input" data-testid="search-data-input"
@press-enter="onPressEnter" @press-enter="onPressEnter"

6
packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue

@ -115,11 +115,11 @@ onMounted(() => {
<NcButton <NcButton
v-e="['c:sort']" v-e="['c:sort']"
:class="{ :class="{
'!border-1 !rounded-lg !h-7': isCalendar, '!border-1 !rounded-lg': isCalendar,
'!border-0 ': !isCalendar, '!border-0': !isCalendar,
}" }"
:disabled="isLocked" :disabled="isLocked"
class="nc-sort-menu-btn nc-toolbar-btn" class="nc-sort-menu-btn nc-toolbar-btn !h-7"
size="small" size="small"
type="secondary" type="secondary"
> >

37
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -170,25 +170,26 @@ const displayValue = computed(() => {
</button> </button>
</NcTooltip> </NcTooltip>
</div> </div>
<template v-if="(!isPublic && !readOnly) || isForm">
<NcTooltip class="z-10 flex">
<template #title> {{ isLinked ? 'Unlink' : 'Link' }}</template>
<NcTooltip class="z-10 flex"> <button
<template #title> {{ isLinked ? 'Unlink' : 'Link' }}</template> tabindex="-1"
class="nc-list-item-link-unlink-btn p-1.5 flex rounded-lg transition-all"
<button :class="{
tabindex="-1" 'bg-gray-200 text-gray-800 hover:(bg-red-100 text-red-500)': isLinked,
class="nc-list-item-link-unlink-btn p-1.5 flex rounded-lg transition-all" 'bg-green-[#D4F7E0] text-[#17803D] hover:bg-green-200': !isLinked,
:class="{ }"
'bg-gray-200 text-gray-800 hover:(bg-red-100 text-red-500)': isLinked, @click="$emit('linkOrUnlink')"
'bg-green-[#D4F7E0] text-[#17803D] hover:bg-green-200': !isLinked, >
}" <div v-if="isLoading" class="flex">
@click="$emit('linkOrUnlink')" <MdiLoading class="flex-none w-4 h-4 !text-brand-500 animate-spin" />
> </div>
<div v-if="isLoading" class="flex"> <GeneralIcon v-else :icon="isLinked ? 'minus' : 'plus'" class="flex-none w-4 h-4 !font-extrabold" />
<MdiLoading class="flex-none w-4 h-4 !text-brand-500 animate-spin" /> </button>
</div> </NcTooltip>
<GeneralIcon v-else :icon="isLinked ? 'minus' : 'plus'" class="flex-none w-4 h-4 !font-extrabold" /> </template>
</button>
</NcTooltip>
</div> </div>
</a-card> </a-card>
</div> </div>

29
packages/nc-gui/composables/useSharedView.ts

@ -124,16 +124,21 @@ export function useSharedView() {
} }
} }
const fetchSharedViewData = async (param: { const fetchSharedViewData = async (
sortsArr: SortType[] param: {
filtersArr: FilterType[] sortsArr: SortType[]
fields?: any[] filtersArr: FilterType[]
sort?: any[] fields?: any[]
where?: string sort?: any[]
/** Query params for nested data */ where?: string
nested?: any /** Query params for nested data */
offset?: number nested?: any
}) => { offset?: number
},
opts?: {
isGroupBy?: boolean
},
) => {
if (!sharedView.value) if (!sharedView.value)
return { return {
list: [], list: [],
@ -142,7 +147,9 @@ export function useSharedView() {
if (!param.offset) { if (!param.offset) {
const page = paginationData.value.page || 1 const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || appInfoDefaultLimit const pageSize = opts?.isGroupBy
? appInfo.value.defaultGroupByLimit?.limitRecord || 10
: paginationData.value.pageSize || appInfoDefaultLimit
param.offset = (page - 1) * pageSize param.offset = (page - 1) * pageSize
param.limit = sharedView.value?.type === ViewTypes.MAP ? 1000 : pageSize param.limit = sharedView.value?.type === ViewTypes.MAP ? 1000 : pageSize
} }

2
packages/nc-gui/composables/useViewGroupBy.ts

@ -382,7 +382,7 @@ const [useProvideViewGroupBy, useViewGroupBy] = useInjectionState(
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }), ...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }), ...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
} as any) } as any)
: await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, ...query }) : await fetchSharedViewData({ sortsArr: sorts.value, filtersArr: nestedFilters.value, ...query }, { isGroupBy: true })
group.count = response.pageInfo.totalRows ?? 0 group.count = response.pageInfo.totalRows ?? 0
group.rows = formatData(response.list) group.rows = formatData(response.list)

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

@ -773,7 +773,8 @@
"limitOptionsSubtext": "Limit options visible to users by selecting available options", "limitOptionsSubtext": "Limit options visible to users by selecting available options",
"clearSelection": "Clear selection", "clearSelection": "Clear selection",
"displayAsProgress": "Display as progress", "displayAsProgress": "Display as progress",
"relationType": "Relation type" "relationType": "Relation type",
"signUpForFree": "Sign up for free"
}, },
"activity": { "activity": {
"renameBase": "Rename Base", "renameBase": "Rename Base",

77
packages/nc-gui/layouts/shared-view.vue

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
const { isLoading, appInfo } = useGlobal() const { isLoading, appInfo } = useGlobal()
const { sharedView } = useSharedView() const { sharedView, allowCSVDownload } = useSharedView()
const router = useRouter() const router = useRouter()
@ -47,44 +47,65 @@ export default {
<template> <template>
<a-layout id="nc-app"> <a-layout id="nc-app">
<a-layout class="!flex-col bg-white"> <a-layout class="!flex-col bg-white">
<a-layout-header class="flex !bg-primary items-center text-white pl-3 pr-4 shadow-lg"> <a-layout-header
<a class="nc-table-topbar flex items-center justify-between !bg-transparent !px-3 !py-2 border-b-1 border-gray-200 !h-[46px]"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105" >
href="https://github.com/nocodb/nocodb" <div class="flex items-center gap-6 h-7 max-w-[calc(100%_-_280px)] xs:max-w-[calc(100%_-_90px)]">
target="_blank" <a
rel="noopener noreferrer" class="transition-all duration-200 cursor-pointer transform hover:scale-105"
> href="https://github.com/nocodb/nocodb"
<a-tooltip placement="bottom"> target="_blank"
<template #title> rel="noopener noreferrer"
{{ appInfo.version }} >
</template> <NcTooltip placement="bottom" class="flex">
<img width="35" alt="NocoDB" src="~/assets/img/icons/256x256-trans.png" /> <template #title>
</a-tooltip> {{ appInfo.version }}
</a> </template>
<img width="96" alt="NocoDB" src="~/assets/img/brand/nocodb.png" class="flex-none min-w-[96px]" />
</NcTooltip>
</a>
<div> <div class="flex items-center gap-2 text-gray-900 text-sm truncate">
<div class="flex justify-center items-center"> <template v-if="isLoading">
<div class="flex items-center gap-2 ml-3 text-white"> <span data-testid="nc-loading">{{ $t('general.loading') }}</span>
<template v-if="isLoading">
<span class="text-white" data-testid="nc-loading">{{ $t('general.loading') }}</span>
<component :is="iconMap.reload" :class="{ 'animate-infinite animate-spin ': isLoading }" /> <component :is="iconMap.reload" :class="{ 'animate-infinite animate-spin ': isLoading }" />
</template> </template>
<div v-else class="text-xl font-semibold truncate text-white nc-shared-view-title flex gap-2 items-center"> <div v-else class="text-sm font-semibold truncate nc-shared-view-title flex gap-2 items-center">
<GeneralViewIcon v-if="sharedView" class="!text-xl" :meta="sharedView" /> <GeneralViewIcon v-if="sharedView" class="h-4 w-4" :meta="sharedView" />
<span class="truncate">
{{ sharedView?.title }} {{ sharedView?.title }}
</div> </span>
</div> </div>
</div> </div>
</div> </div>
<div class="flex-1" /> <div class="flex items-center gap-3">
</a-layout-header> <LazySmartsheetToolbarExport v-if="allowCSVDownload" />
<div class="w-full overflow-hidden" style="height: calc(100vh - var(--topbar-height))"> <a href="https://app.nocodb.com/#/signin" target="_blank" class="!no-underline xs:hidden" rel="noopener">
<NcButton size="xs"> {{ $t('labels.signUpForFree') }} </NcButton>
</a>
</div>
</a-layout-header>
<div class="w-full overflow-hidden" style="height: calc(100vh - (var(--topbar-height) - 3.6px))">
<slot /> <slot />
</div> </div>
</a-layout> </a-layout>
</a-layout> </a-layout>
</template> </template>
<style lang="scss" scoped>
#nc-app {
.ant-layout-header {
@apply !h-[46px];
line-height: unset;
}
:deep(.nc-table-toolbar) {
@apply px-2;
}
}
</style>

2
packages/nc-gui/lib/types.ts

@ -197,7 +197,7 @@ interface Users {
type ViewPageType = 'view' | 'webhook' | 'api' | 'field' | 'relation' type ViewPageType = 'view' | 'webhook' | 'api' | 'field' | 'relation'
type NcButtonSize = 'xxsmall' | 'xsmall' | 'small' | 'medium' type NcButtonSize = 'xxsmall' | 'xsmall' | 'small' | 'medium' | 'xs'
interface SidebarTableNode extends TableType { interface SidebarTableNode extends TableType {
isMetaLoading?: boolean isMetaLoading?: boolean

3
packages/nc-gui/pages/index/[typeOrId]/calendar/[viewId]/index.vue

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ViewTypes } from 'nocodb-sdk'
definePageMeta({ definePageMeta({
public: true, public: true,
@ -27,7 +28,7 @@ try {
<template> <template>
<div v-if="showPassword"> <div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" /> <LazySharedViewAskPassword v-model="showPassword" :view-type="ViewTypes.CALENDAR" />
</div> </div>
<LazySharedViewCalendar v-else /> <LazySharedViewCalendar v-else />
</template> </template>

74
packages/nc-gui/pages/index/[typeOrId]/form/[viewId].vue

@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import type { InputPassword } from 'ant-design-vue'
definePageMeta({ definePageMeta({
public: true, public: true,
pageType: 'shared-view', pageType: 'shared-view',
@ -39,6 +42,10 @@ watch(
password.value = form.password password.value = form.password
}, },
) )
const focus: VNodeRef = (el: typeof InputPassword) => {
return el && el?.focus?.()
}
</script> </script>
<template> <template>
@ -55,25 +62,53 @@ watch(
:footer="null" :footer="null"
:mask-closable="false" :mask-closable="false"
wrap-class-name="nc-modal-shared-form-password-dlg" wrap-class-name="nc-modal-shared-form-password-dlg"
:mask-style="{
backgroundColor: 'rgba(255, 255, 255, 0.64)',
backdropFilter: 'blur(8px)',
}"
@close="passwordDlg = false" @close="passwordDlg = false"
> >
<div class="w-full flex flex-col gap-4"> <div class="flex flex-col gap-5">
<h2 class="text-xl font-semibold">{{ $t('msg.thisSharedViewIsProtected') }}</h2> <div class="flex flex-row items-center gap-x-2 text-base font-weight-700 text-gray-800">
<GeneralIcon icon="ncKey" class="!text-base w-5 h-5" />
<a-form layout="vertical" no-style :model="form" @finish="loadSharedView"> {{ $t('msg.thisSharedViewIsProtected') }}
<a-form-item name="password" :rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]"> </div>
<a-input-password v-model:value="form.password" size="large" :placeholder="$t('msg.enterPassword')" />
<a-form :model="form" @finish="loadSharedView">
<a-form-item
name="password"
:rules="[{ required: true, message: $t('msg.error.signUpRules.passwdRequired') }]"
class="!mb-0"
>
<a-input-password
:ref="focus"
v-model:value="form.password"
class="!rounded-lg !text-small"
hide-details
:placeholder="$t('msg.enterPassword')"
/>
<Transition name="layout">
<div v-if="passwordError" class="mb-2 text-sm text-red-500">{{ passwordError }}</div>
</Transition>
</a-form-item> </a-form-item>
<Transition name="layout">
<div v-if="passwordError" class="mb-2 text-sm text-red-500">{{ passwordError }}</div>
</Transition>
<!-- Unlock -->
<button type="submit" class="mt-4 scaling-btn bg-opacity-100">{{ $t('general.unlock') }}</button>
</a-form> </a-form>
<div class="flex flex-row justify-end gap-x-2">
<NcButton
:disabled="!form.password"
type="primary"
size="small"
html-type="submit"
class="!px-2"
data-testid="nc-shared-view-password-submit-btn"
@click="loadSharedView"
>{{ $t('objects.view') }}
<template #loading> {{ $t('msg.verifyingPassword') }}</template>
</NcButton>
</div>
</div> </div>
</a-modal> </a-modal>
<img v-if="passwordDlg" alt="view image" src="~/assets/img/views/form.png" class="fixed inset-0 w-full h-full" />
</NuxtLayout> </NuxtLayout>
</div> </div>
</template> </template>
@ -94,17 +129,4 @@ watch(
} }
} }
} }
.nc-modal-shared-form-password-dlg {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid border-primary border-opacity-50 rounded;
}
.password {
input {
@apply !border-none !m-0;
}
}
}
</style> </style>

2
packages/nc-gui/pages/index/[typeOrId]/form/[viewId]/index.vue

@ -26,7 +26,7 @@ router.afterEach((to) => shouldRedirect(to.name as string))
<template> <template>
<div <div
class="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-200 hover-scrollbar-thumb-gray-300 h-[100vh] overflow-y-auto overflow-x-hidden flex flex-col color-transition p-4 lg:p-10 nc-form-view min-h-[600px]" class="scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-200 hover-scrollbar-thumb-gray-300 h-[100vh] overflow-y-auto overflow-x-hidden flex flex-col color-transition p-4 lg:p-6 nc-form-view min-h-[600px]"
:class="{ :class="{
'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode, 'children:(!h-auto my-auto)': sharedViewMeta?.surveyMode,
}" }"

3
packages/nc-gui/pages/index/[typeOrId]/gallery/[viewId]/index.vue

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ViewTypes } from 'nocodb-sdk'
definePageMeta({ definePageMeta({
public: true, public: true,
@ -26,7 +27,7 @@ try {
<template> <template>
<div v-if="showPassword"> <div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" /> <LazySharedViewAskPassword v-model="showPassword" :view-type="ViewTypes.GALLERY" />
</div> </div>
<LazySharedViewGallery v-else /> <LazySharedViewGallery v-else />
</template> </template>

3
packages/nc-gui/pages/index/[typeOrId]/kanban/[viewId]/index.vue

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ViewTypes } from 'nocodb-sdk'
definePageMeta({ definePageMeta({
public: true, public: true,
@ -27,7 +28,7 @@ try {
<template> <template>
<div v-if="showPassword"> <div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" /> <LazySharedViewAskPassword v-model="showPassword" :view-type="ViewTypes.KANBAN" />
</div> </div>
<LazySharedViewKanban v-else /> <LazySharedViewKanban v-else />
</template> </template>

3
packages/nc-gui/pages/index/[typeOrId]/map/[viewId]/index.vue

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ViewTypes } from 'nocodb-sdk'
definePageMeta({ definePageMeta({
public: true, public: true,
@ -26,7 +27,7 @@ try {
<template> <template>
<div v-if="showPassword"> <div v-if="showPassword">
<LazySharedViewAskPassword v-model="showPassword" /> <LazySharedViewAskPassword v-model="showPassword" :view-type="ViewTypes.MAP" />
</div> </div>
<LazySharedViewMap v-else /> <LazySharedViewMap v-else />
</template> </template>

4
packages/nc-gui/pages/index/[typeOrId]/view/[viewId].vue

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
definePageMeta({ definePageMeta({
public: true, public: true,
requiresAuth: false, requiresAuth: false,
@ -37,5 +39,5 @@ onMounted(async () => {
<LazySharedViewAskPassword v-model="showPassword" /> <LazySharedViewAskPassword v-model="showPassword" />
</div> </div>
<LazySharedViewGrid v-else-if="meta" /> <LazySharedViewGrid v-else-if="meta" :view-type="ViewTypes.GRID" />
</template> </template>

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

@ -190,6 +190,7 @@ import NcHelp from '~icons/nc-icons/help'
import NcAlertTriangle from '~icons/nc-icons/alert-triangle' import NcAlertTriangle from '~icons/nc-icons/alert-triangle'
import NcAudit from '~icons/nc-icons/audit' import NcAudit from '~icons/nc-icons/audit'
import NcMessageCircle from '~icons/nc-icons/message-circle' import NcMessageCircle from '~icons/nc-icons/message-circle'
import NcKey from '~icons/nc-icons/key'
// keep it for reference // keep it for reference
// todo: remove it after all icons are migrated // todo: remove it after all icons are migrated
@ -612,6 +613,7 @@ export const iconMap = {
alertTriangle: NcAlertTriangle, alertTriangle: NcAlertTriangle,
audit: NcAudit, audit: NcAudit,
messageCircle: NcMessageCircle, messageCircle: NcMessageCircle,
ncKey: NcKey,
} }
export const getMdiIcon = (type: string): any => { export const getMdiIcon = (type: string): any => {

14
tests/playwright/pages/Dashboard/common/Toolbar/CalendarViewMode.ts

@ -12,10 +12,18 @@ export class ToolbarCalendarViewModePage extends BasePage {
return this.rootPage.getByTestId('nc-calendar-view-mode'); return this.rootPage.getByTestId('nc-calendar-view-mode');
} }
getViewTab({ title }: { title: string }) {
return this.get().getByTestId(`nc-calendar-view-mode-${title}`);
}
async changeCalendarView({ title }: { title: string }) { async changeCalendarView({ title }: { title: string }) {
await this.get().click({ force: true }); if (await this.getViewTab({ title }).isVisible()) {
await this.rootPage.waitForTimeout(500); await this.getViewTab({ title }).click({ force: true });
} else {
await this.get().click({ force: true });
await this.rootPage.waitForTimeout(500);
await this.rootPage.locator('.rc-virtual-list-holder-inner > div').locator(`text="${title}"`).click(); await this.rootPage.locator('.rc-virtual-list-holder-inner > div').locator(`text="${title}"`).click();
}
} }
} }

22
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -195,28 +195,6 @@ export class ToolbarPage extends BasePage {
await this.get().locator(`.nc-toolbar-btn.nc-add-new-row-btn`).click(); await this.get().locator(`.nc-toolbar-btn.nc-add-new-row-btn`).click();
} }
async clickDownload(type: string, verificationFile = 'expectedData.txt') {
await this.get().locator(`.nc-toolbar-btn.nc-actions-menu-btn`).click();
const [download] = await Promise.all([
// Start waiting for the download
this.rootPage.waitForEvent('download'),
// Perform the action that initiates download
this.rootPage
.locator(`.nc-dropdown-actions-menu`)
.locator(`li.ant-dropdown-menu-item:has-text("${type}")`)
.click(),
]);
// Save downloaded file somewhere
await download.saveAs('./output/at.txt');
// verify downloaded content against expected content
const expectedData = fs.readFileSync(`./fixtures/${verificationFile}`, 'utf8').replace(/\r/g, '').split('\n');
const file = fs.readFileSync('./output/at.txt', 'utf8').replace(/\r/g, '').split('\n');
expect(file).toEqual(expectedData);
}
async clickRowHeight() { async clickRowHeight() {
// ant-btn nc-height-menu-btn nc-toolbar-btn // ant-btn nc-height-menu-btn nc-toolbar-btn
await this.get().locator(`.nc-toolbar-btn.nc-height-menu-btn`).click(); await this.get().locator(`.nc-toolbar-btn.nc-height-menu-btn`).click();

26
tests/playwright/pages/Dashboard/common/Topbar/index.ts

@ -1,4 +1,6 @@
import { Locator } from '@playwright/test'; import { expect, Locator } from '@playwright/test';
import * as fs from 'fs';
import BasePage from '../../../Base'; import BasePage from '../../../Base';
import { GridPage } from '../../Grid'; import { GridPage } from '../../Grid';
import { GalleryPage } from '../../Gallery'; import { GalleryPage } from '../../Gallery';
@ -98,4 +100,26 @@ export class TopbarPage extends BasePage {
await this.get().locator(`.nc-icon-reload`).click(); await this.get().locator(`.nc-icon-reload`).click();
await this.rootPage.waitForLoadState('networkidle'); await this.rootPage.waitForLoadState('networkidle');
} }
async clickDownload(type: string, verificationFile = 'expectedData.txt') {
await this.get().locator(`.nc-toolbar-btn.nc-actions-menu-btn`).click();
const [download] = await Promise.all([
// Start waiting for the download
this.rootPage.waitForEvent('download'),
// Perform the action that initiates download
this.rootPage
.locator(`.nc-dropdown-actions-menu`)
.locator(`li.ant-dropdown-menu-item:has-text("${type}")`)
.click(),
]);
// Save downloaded file somewhere
await download.saveAs('./output/at.txt');
// verify downloaded content against expected content
const expectedData = fs.readFileSync(`./fixtures/${verificationFile}`, 'utf8').replace(/\r/g, '').split('\n');
const file = fs.readFileSync('./output/at.txt', 'utf8').replace(/\r/g, '').split('\n');
expect(file).toEqual(expectedData);
}
} }

6
tests/playwright/tests/db/views/viewGridShare.spec.ts

@ -246,7 +246,7 @@ test.describe('Shared view', () => {
**/ **/
// verify download // verify download
await sharedPage.grid.toolbar.clickDownload( await sharedPage.grid.topbar.clickDownload(
'Download CSV', 'Download CSV',
isSqlite(context) || isPg(context) ? 'expectedDataSqlite.txt' : 'expectedData.txt' isSqlite(context) || isPg(context) ? 'expectedDataSqlite.txt' : 'expectedData.txt'
); );
@ -284,12 +284,12 @@ test.describe('Shared view', () => {
// verify if password request modal exists // verify if password request modal exists
const sharedPage2 = new DashboardPage(page, context.base); const sharedPage2 = new DashboardPage(page, context.base);
await sharedPage2.rootPage.locator('input[placeholder="Enter password"]').fill('incorrect p@ssword'); await sharedPage2.rootPage.locator('input[placeholder="Enter password"]').fill('incorrect p@ssword');
await sharedPage2.rootPage.click('button:has-text("Unlock")'); await sharedPage2.rootPage.click('button[data-testid="nc-shared-view-password-submit-btn"]');
await sharedPage2.verifyToast({ message: 'INVALID_SHARED_VIEW_PASSWORD' }); await sharedPage2.verifyToast({ message: 'INVALID_SHARED_VIEW_PASSWORD' });
// correct password // correct password
await sharedPage2.rootPage.locator('input[placeholder="Enter password"]').fill('p@ssword'); await sharedPage2.rootPage.locator('input[placeholder="Enter password"]').fill('p@ssword');
await sharedPage2.rootPage.click('button:has-text("Unlock")'); await sharedPage2.rootPage.click('button[data-testid="nc-shared-view-password-submit-btn"]');
// verify if download button is disabled // verify if download button is disabled
await sharedPage2.grid.toolbar.verifyDownloadDisabled(); await sharedPage2.grid.toolbar.verifyDownloadDisabled();

Loading…
Cancel
Save