Browse Source

Merge pull request #6474 from nocodb/nc-feat/mobile-responsive

Added Mobile responsive
pull/6478/head
Muhammed Mustafa 12 months ago committed by GitHub
parent
commit
3cc74de6f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      packages/nc-gui/assets/style.scss
  2. 2
      packages/nc-gui/components/dashboard/Sidebar.vue
  3. 21
      packages/nc-gui/components/dashboard/Sidebar/Header.vue
  4. 4
      packages/nc-gui/components/dashboard/Sidebar/TopSection.vue
  5. 43
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  6. 4
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  7. 17
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  8. 3
      packages/nc-gui/components/dashboard/TreeView/TableList.vue
  9. 35
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  10. 37
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  11. 4
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  12. 51
      packages/nc-gui/components/dashboard/View.vue
  13. 3
      packages/nc-gui/components/dlg/ViewCreate.vue
  14. 2
      packages/nc-gui/components/general/EmojiPicker.vue
  15. 15
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  16. 17
      packages/nc-gui/components/general/ShareProject.vue
  17. 38
      packages/nc-gui/components/nc/Button.vue
  18. 816
      packages/nc-gui/components/smartsheet/Form.vue
  19. 8
      packages/nc-gui/components/smartsheet/Pagination.vue
  20. 41
      packages/nc-gui/components/smartsheet/Toolbar.vue
  21. 21
      packages/nc-gui/components/smartsheet/Topbar.vue
  22. 23
      packages/nc-gui/components/smartsheet/grid/Table.vue
  23. 7
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  24. 7
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  25. 1
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  26. 2
      packages/nc-gui/components/smartsheet/toolbar/MappedBy.vue
  27. 27
      packages/nc-gui/components/smartsheet/toolbar/SearchData.vue
  28. 7
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  29. 1
      packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue
  30. 75
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  31. 9
      packages/nc-gui/components/tabs/Smartsheet.vue
  32. 6
      packages/nc-gui/composables/useTableNew.ts
  33. 10
      packages/nc-gui/layouts/base.vue
  34. 2
      packages/nc-gui/layouts/dashboard.vue
  35. 2
      packages/nc-gui/lib/constants.ts
  36. 4
      packages/nc-gui/pages/index/[typeOrId]/[projectId]/index/index/index.vue
  37. 70
      packages/nc-gui/store/config.ts
  38. 17
      packages/nc-gui/store/sidebar.ts
  39. 9
      packages/nc-gui/store/views.ts
  40. 3
      packages/nc-gui/utils/iconUtils.ts
  41. 11
      packages/nc-gui/windi.config.ts

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

@ -48,6 +48,7 @@ main {
@apply m-0 h-full w-full bg-white;
}
.nc-input-md {
@apply !rounded-lg !py-2 !px-3 mb-1;
}
@ -596,6 +597,34 @@ input[type='number'] {
@apply !block !py-1.5;
}
.nc-sidebar-node {
@apply !xs:(min-h-12 max-h-12 hover:bg-gray-50 ml-1.5);
.nc-emoji {
@apply xs:(text-lg);
}
.material-symbols, .nc-icon {
@apply !xs:(text-xl -mt-0.25);
}
.nc-sidebar-node-title {
@apply xs:(text-base);
}
.nc-sidebar-node-btn:not(.nc-sidebar-expand) {
@apply !xs:(hidden)
}
}
.nc-button.ant-btn.nc-sidebar-node-btn {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}
.nc-button.ant-btn.nc-sidebar-node-btn.nc-sidebar-expand {
@apply xs:(opacity-100 hover:bg-gray-50);
.nc-icon {
@apply xs:(visible opacity-100 !text-gray-500)
}
}

2
packages/nc-gui/components/dashboard/Sidebar.vue

@ -52,7 +52,7 @@ onUnmounted(() => {
>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase" style="height: var(--sidebar-bottom-height)">
<div v-if="!isSharedBase">
<DashboardSidebarUserInfo />
</div>
</div>

21
packages/nc-gui/components/dashboard/Sidebar/Header.vue

@ -4,11 +4,19 @@ const workspaceStore = useWorkspace()
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
const { activeViewTitleOrId } = storeToRefs(useViewsStore())
const { activeTableId } = storeToRefs(useTablesStore())
const { isMobileMode } = useGlobal()
const showSidebarBtn = computed(() => !(isMobileMode.value && !activeViewTitleOrId.value && !activeTableId.value))
</script>
<template>
<div
class="flex items-center px-2 nc-sidebar-header py-1.2 w-full border-b-1 border-gray-200 group"
class="flex items-center nc-sidebar-header w-full border-b-1 border-gray-200 group md:(px-2 py-1.2) xs:(px-1 py-1)"
:data-workspace-title="activeWorkspace?.title"
style="height: var(--topbar-height)"
>
@ -18,7 +26,7 @@ const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
<div class="flex flex-grow min-w-1"></div>
<NcTooltip
class="flex opacity-0 group-hover:opacity-100 transition-opacity duration-50"
class="flex"
:class="{
'!opacity-100': !isLeftSidebarOpen,
}"
@ -33,13 +41,16 @@ const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore)
}}
</template>
<NcButton
type="text"
size="small"
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !hover:bg-gray-200"
v-if="showSidebarBtn"
:type="isMobileMode ? 'secondary' : 'text'"
:size="isMobileMode ? 'medium' : 'small'"
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !xs:(h-10.5 max-h-10.5 max-w-10.5) !md:(hover:bg-gray-200)"
@click="isLeftSidebarOpen = !isLeftSidebarOpen"
>
<div class="flex items-center text-inherit">
<GeneralIcon v-if="isMobileMode" icon="close" />
<GeneralIcon
v-else
icon="doubleLeftArrow"
class="duration-150 transition-all !text-lg -mt-0.5"
:class="{

4
packages/nc-gui/components/dashboard/Sidebar/TopSection.vue

@ -50,7 +50,7 @@ const navigateToSettings = () => {
v-if="isUIAllowed('workspaceSettings')"
type="text"
size="small"
class="nc-sidebar-top-button"
class="nc-sidebar-top-button !xs:hidden"
data-testid="nc-sidebar-team-settings-btn"
:centered="false"
:class="{
@ -68,7 +68,7 @@ const navigateToSettings = () => {
v-model:is-open="isCreateProjectOpen"
modal
type="text"
class="nc-sidebar-top-button !hover:bg-gray-200"
class="nc-sidebar-top-button !hover:bg-gray-200 !xs:hidden"
data-testid="nc-sidebar-create-project-btn"
>
<div class="gap-x-2 flex flex-row w-full items-center !font-normal">

43
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -19,6 +19,8 @@ const isAuthTokenCopied = ref(false)
const isLoggingOut = ref(false)
const { isMobileMode } = useGlobal()
const logout = async () => {
isLoggingOut.value = true
try {
@ -84,13 +86,15 @@ onMounted(() => {
<GeneralIcon v-else icon="signout" class="menu-icon" />
<span class="menu-btn"> Log Out </span>
</NcMenuItem>
<NcDivider />
<a href="https://docs.nocodb.com" target="_blank" class="!underline-transparent">
<NcMenuItem>
<GeneralIcon icon="help" class="menu-icon mt-0.5" />
<span class="menu-btn"> Help Center </span>
</NcMenuItem>
</a>
<template v-if="!isMobileMode">
<NcDivider />
<a href="https://docs.nocodb.com" target="_blank" class="!underline-transparent">
<NcMenuItem>
<GeneralIcon icon="help" class="menu-icon mt-0.5" />
<span class="menu-btn"> Help Center </span>
</NcMenuItem>
</a>
</template>
<NcDivider />
<a href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent">
<NcMenuItem class="social-icon-wrapper">
@ -130,21 +134,24 @@ onMounted(() => {
</a-popover>
</template>
<NcDivider />
<NcMenuItem @click="onCopy">
<GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" />
<GeneralIcon v-else icon="copy" class="menu-icon" />
<template v-if="isAuthTokenCopied"> Copied Auth Token </template>
<template v-else> Copy Auth Token </template>
</NcMenuItem>
<nuxt-link v-e="['c:navbar:user:email']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="settings" class="menu-icon" /> Account Settings </NcMenuItem>
</nuxt-link>
<template v-if="!isMobileMode">
<NcDivider />
<NcMenuItem @click="onCopy">
<GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" />
<GeneralIcon v-else icon="copy" class="menu-icon" />
<template v-if="isAuthTokenCopied"> Copied Auth Token </template>
<template v-else> Copy Auth Token </template>
</NcMenuItem>
<nuxt-link v-e="['c:navbar:user:email']" class="!no-underline" to="/account/profile">
<NcMenuItem> <GeneralIcon icon="settings" class="menu-icon" /> Account Settings </NcMenuItem>
</nuxt-link>
</template>
</NcMenu>
</template>
</NcDropdown>
<div v-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div>
<template v-if="isMobileMode"></template>
<div v-else-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div>
<div v-else-if="isMounted" class="flex flex-row justify-between pt-1 truncate">
<div class="flex flex-wrap mb-1">
<GithubButton

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

@ -43,7 +43,9 @@ function onOpenModal({
refreshCommandPalette()
await loadViews()
await loadViews({
force: true,
})
navigateToView({
view,

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

@ -35,6 +35,8 @@ const project = inject(ProjectInj)!
const projectsStore = useProjects()
const { isMobileMode } = useGlobal()
const { loadProject, loadProjects, createProject: _createProject, updateProject, getProjectMetaInfo } = projectsStore
const { projects } = storeToRefs(projectsStore)
@ -234,6 +236,9 @@ const onProjectClick = async (project: NcProject, ignoreNavigation?: boolean, to
return
}
ignoreNavigation = isMobileMode.value || ignoreNavigation
toggleIsExpanded = isMobileMode.value || toggleIsExpanded
if (toggleIsExpanded) {
project.isExpanded = !project.isExpanded
} else {
@ -379,21 +384,21 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<div
ref="projectNodeRefs"
:class="{
'bg-primary-selected active': activeProjectId === project.id && projectViewOpen,
'bg-primary-selected active': activeProjectId === project.id && projectViewOpen && !isMobileMode,
'hover:bg-gray-200': !(activeProjectId === project.id && projectViewOpen),
}"
:data-testid="`nc-sidebar-project-title-${project.title}`"
class="project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
class="nc-sidebar-node project-title-node h-7.25 flex-grow rounded-md group flex items-center w-full pr-1"
>
<NcButton
type="text"
size="xxsmall"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75"
class="nc-sidebar-node-btn nc-sidebar-expand ml-0.75 !xs:visible"
@click="onProjectClick(project, true, true)"
>
<GeneralIcon
icon="triangleFill"
class="absolute top-2.25 left-2 group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90"
class="group-hover:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.75 rotate-90 !xs:visible"
:class="{ '!rotate-180': project.isExpanded, '!visible': isOptionsOpen }"
/>
</NcButton>
@ -426,7 +431,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
ref="input"
v-model="tempTitle"
class="flex-grow leading-1 outline-0 ring-none capitalize !text-inherit !bg-transparent w-4/5"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen }"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen && !isMobileMode }"
@click.stop
@keyup.enter="updateProjectTitle"
@keyup.esc="updateProjectTitle"
@ -434,7 +439,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
/>
<span
v-else
class="capitalize text-ellipsis overflow-hidden select-none"
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
:class="{ 'text-black font-semibold': activeProjectId === project.id && projectViewOpen }"
@click="onProjectClick(project)"

3
packages/nc-gui/components/dashboard/TreeView/TableList.vue

@ -21,6 +21,8 @@ const baseIndex = toRef(props, 'baseIndex')
const base = computed(() => project.value?.bases?.[baseIndex.value])
const { isMobileMode } = useGlobal()
const { projectTables } = storeToRefs(useTablesStore())
const tables = computed(() => projectTables.value.get(project.value.id!) ?? [])
@ -44,6 +46,7 @@ const sortables: Record<string, Sortable> = {}
const initSortable = (el: Element) => {
const base_id = el.getAttribute('nc-base')
if (!base_id) return
if (isMobileMode.value) return
if (sortables[base_id]) sortables[base_id].destroy()
Sortable.create(el as HTMLLIElement, {

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

@ -20,7 +20,7 @@ const project = toRef(props, 'project')
const table = toRef(props, 'table')
const baseIndex = toRef(props, 'baseIndex')
const { openTable } = useTableNew({
const { openTable: _openTable } = useTableNew({
projectId: project.value.id!,
})
@ -28,6 +28,8 @@ const route = useRoute()
const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal()
const tabStore = useTabs()
const { updateTab } = tabStore
@ -102,6 +104,18 @@ const onExpand = async () => {
}
}
const onOpenTable = async () => {
isLoading.value = true
try {
await _openTable(table.value)
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
isExpanded.value = true
}
}
watch(
() => activeView.value?.id,
() => {
@ -131,7 +145,7 @@ const isTableOpened = computed(() => {
:data-active="openedTableId === table.id"
>
<GeneralTooltip
class="nc-tree-item-inner pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-12': baseIndex !== 0,
@ -145,13 +159,13 @@ const isTableOpened = computed(() => {
class="table-context flex items-center gap-1 h-full"
:data-testid="`nc-tbl-side-node-${table.title}`"
@contextmenu="setMenuContext('table', table)"
@click="openTable(table)"
@click="onOpenTable"
>
<div class="flex flex-row h-full items-center">
<NcButton type="text" size="xxsmall" class="nc-sidebar-node-btn" @click.stop="onExpand">
<NcButton type="text" size="xxsmall" class="nc-sidebar-node-btn nc-sidebar-expand" @click.stop="onExpand">
<GeneralIcon
icon="triangleFill"
class="nc-sidebar-base-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90 hover:bg-"
class="nc-sidebar-base-node-btns group-hover:visible invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 !text-gray-600 rotate-90"
:class="{ '!rotate-180': isExpanded }"
/>
</NcButton>
@ -200,7 +214,7 @@ const isTableOpened = computed(() => {
</div>
<span
class="nc-tbl-title capitalize text-ellipsis overflow-hidden select-none"
class="nc-tbl-title nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none"
:class="{
'text-black !font-medium': isTableOpened,
}"
@ -263,7 +277,14 @@ const isTableOpened = computed(() => {
</template>
</NcDropdown>
<DashboardTreeViewCreateViewBtn v-if="isUIAllowed('viewCreateOrEdit')">
<NcButton type="text" size="xxsmall" class="nc-create-view-btn nc-sidebar-node-btn">
<NcButton
type="text"
size="xxsmall"
class="nc-create-view-btn nc-sidebar-node-btn"
:class="{
'!md:(visible opacity-100)': openedTableId === table.id,
}"
>
<GeneralIcon icon="plus" class="text-xl leading-5" style="-webkit-text-stroke: 0.15px" />
</NcButton>
</DashboardTreeViewCreateViewBtn>

37
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -32,7 +32,7 @@ const emits = defineEmits<Emits>()
const project = inject(ProjectInj)!
const table = inject(SidebarTableInj)!
const { isUIAllowed } = useRoles()
const { isMobileMode } = useGlobal()
const { $e } = useNuxtApp()
@ -178,6 +178,7 @@ async function onSortEnd(evt: SortableEvent, undo = false) {
const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy()
if (isMobileMode.value) return
sortable = new Sortable(el, {
// handle: '.nc-drag-icon',
@ -268,6 +269,7 @@ function openDeleteDialog(view: ViewType) {
await loadViews({
tableId: table.value.id!,
force: true,
})
},
})
@ -324,7 +326,9 @@ function onOpenModal({
refreshCommandPalette()
await loadViews()
await loadViews({
force: true,
})
navigateToView({
view,
@ -346,27 +350,16 @@ function onOpenModal({
</script>
<template>
<DashboardTreeViewCreateViewBtn
v-if="isUIAllowed('viewCreateOrEdit')"
:overlay-class-name="isDefaultBase ? '!left-18 !min-w-42' : '!left-25 !min-w-42'"
<div
v-if="!views.length"
class="text-gray-500 my-1.75"
:class="{
'ml-19.25': isDefaultBase,
'ml-24.75': !isDefaultBase,
}"
>
<NcButton
type="text"
size="xsmall"
class="!w-full !py-0 !h-7 !text-gray-500 !hover:(bg-transparent font-normal text-brand-500) !font-normal !text-sm"
:centered="false"
>
<GeneralIcon
icon="plus"
class="mr-2"
:class="{
'ml-18.75': isDefaultBase,
'ml-24.25': !isDefaultBase,
}"
/>
<span class="text-sm">New View</span>
</NcButton>
</DashboardTreeViewCreateViewBtn>
No Views
</div>
<a-menu
ref="menuRef"

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

@ -210,7 +210,7 @@ function onRef(el: HTMLElement) {
<template>
<a-menu-item
class="!min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
:class="{
'!pl-18': isDefaultBase,
'!pl-23.5': !isDefaultBase,
@ -253,7 +253,7 @@ function onRef(el: HTMLElement) {
<div
v-else
class="capitalize text-ellipsis overflow-hidden select-none w-full"
class="nc-sidebar-node-title capitalize text-ellipsis overflow-hidden select-none w-full"
data-testid="sidebar-view-title"
:class="{
'font-medium': activeView?.id === vModel.id,

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

@ -5,6 +5,8 @@ import 'splitpanes/dist/splitpanes.css'
const router = useRouter()
const route = router.currentRoute
const { isMobileMode } = storeToRefs(useConfigStore())
const {
isLeftSidebarOpen,
leftSidebarWidthPercent,
@ -14,11 +16,9 @@ const {
const wrapperRef = ref<HTMLDivElement>()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const animationDuration = 250
const viewportWidth = ref(window.innerWidth)
const sidebarWidth = computed(() => (sideBarSize.value.old * viewportWidth.value) / 100)
const currentSidebarSize = computed({
get: () => sideBarSize.value.current,
set: (val) => {
@ -27,6 +27,30 @@ const currentSidebarSize = computed({
},
})
const { handleSidebarOpenOnMobileForNonViews } = useConfigStore()
const contentSize = computed(() => 100 - sideBarSize.value.current)
const mobileNormalizedSidebarSize = computed(() => {
if (isMobileMode.value) {
return isLeftSidebarOpen.value ? 100 : 0
}
return currentSidebarSize.value
})
const mobileNormalizedContentSize = computed(() => {
if (isMobileMode.value) {
return isLeftSidebarOpen.value ? 0 : 100
}
return contentSize.value
})
const sidebarWidth = computed(() =>
isMobileMode.value ? viewportWidth.value : (sideBarSize.value.old * viewportWidth.value) / 100,
)
watch(currentSidebarSize, () => {
leftSidebarWidthPercent.value = currentSidebarSize.value
})
@ -52,6 +76,7 @@ watch(isLeftSidebarOpen, () => {
})
function handleMouseMove(e: MouseEvent) {
if (isMobileMode.value) return
if (!wrapperRef.value) return
if (sidebarState.value === 'openEnd') return
@ -89,6 +114,14 @@ watch(route, () => {
isLeftSidebarOpen.value = true
}
})
watch(isMobileMode, () => {
isLeftSidebarOpen.value = !isMobileMode.value
})
onMounted(() => {
handleSidebarOpenOnMobileForNonViews()
})
</script>
<template>
@ -99,11 +132,17 @@ watch(route, () => {
}"
@resize="currentSidebarSize = $event[0].size"
>
<Pane min-size="15%" :size="currentSidebarSize" max-size="40%" class="nc-sidebar-splitpane relative !overflow-visible">
<Pane
min-size="15%"
:size="mobileNormalizedSidebarSize"
max-size="40%"
class="nc-sidebar-splitpane relative !overflow-visible"
>
<div
ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible"
:class="{
'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen,
'hide-sidebar': ['hiddenStart', 'hiddenEnd', 'peekCloseEnd'].includes(sidebarState),
}"
@ -114,7 +153,7 @@ watch(route, () => {
<slot name="sidebar" />
</div>
</Pane>
<Pane :size="contentSize">
<Pane :size="mobileNormalizedContentSize">
<slot name="content" />
</Pane>
</Splitpanes>
@ -126,6 +165,10 @@ watch(route, () => {
width: calc(100% + 4px);
}
.mobile.nc-sidebar-wrapper.minimized-height > * {
@apply !h-full;
}
.nc-sidebar-wrapper > * {
transition: all 0.2s ease-in-out;
@apply z-10 absolute;

3
packages/nc-gui/components/dlg/ViewCreate.vue

@ -264,7 +264,8 @@ onMounted(async () => {
v-model:value="form.fk_grp_col_id"
class="w-full nc-kanban-grouping-field-select"
:disabled="groupingFieldColumnId || isMetaLoading"
:loading="true"
:loading="isMetaLoading"
:options="viewSelectFieldOptions"
placeholder="Select a Grouping Field"
not-found-content="No Single Select Field can be found. Please create one first."
/>

2
packages/nc-gui/components/general/EmojiPicker.vue

@ -80,7 +80,7 @@ const showClearButton = computed(() => {
<template>
<a-dropdown v-model:visible="isOpen" :trigger="['click']" :disabled="readonly">
<div
class="flex flex-row justify-center items-center select-none rounded-md"
class="flex flex-row justify-center items-center select-none rounded-md nc-emoji"
:class="{
'hover:bg-gray-500 hover:bg-opacity-15 cursor-pointer': !readonly,
'bg-gray-500 bg-opacity-15': isOpen,

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

@ -2,6 +2,8 @@
const { isLeftSidebarOpen: _isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const isLeftSidebarOpen = ref(_isLeftSidebarOpen.value)
const { isMobileMode } = useGlobal()
watch(_isLeftSidebarOpen, (val) => {
if (val) {
isLeftSidebarOpen.value = true
@ -25,8 +27,8 @@ const onClick = () => {
hide-on-click
class="transition-all duration-100"
:class="{
'!w-0 !opacity-0': isLeftSidebarOpen,
'!w-8 !opacity-100': !isLeftSidebarOpen,
'opacity-0 max-w-0': !isMobileMode && isLeftSidebarOpen,
'opacity-100': isMobileMode || !isLeftSidebarOpen,
}"
>
<template #title>
@ -37,16 +39,17 @@ const onClick = () => {
}}
</template>
<NcButton
type="text"
size="small"
: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': isLeftSidebarOpen,
'invisible !w-0': !isMobileMode && isLeftSidebarOpen,
}"
@click="onClick"
>
<div class="flex items-center text-inherit">
<GeneralIcon icon="doubleRightArrow" class="duration-150 transition-all !text-lg -mt-0.25" />
<GeneralIcon v-if="isMobileMode" icon="menu" class="text-lg -mt-0.25" />
<GeneralIcon v-else icon="doubleRightArrow" class="duration-150 transition-all !text-lg -mt-0.25" />
</div>
</NcButton>
</NcTooltip>

17
packages/nc-gui/components/general/ShareProject.vue

@ -8,6 +8,8 @@ interface Props {
const { disabled, isViewToolbar } = defineProps<Props>()
const { isMobileMode } = useGlobal()
const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore())
@ -41,12 +43,23 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
data-testid="share-project-button"
:data-sharetype="visibility"
>
<NcButton size="small" class="z-10 !rounded-lg !px-2" type="primary" :disabled="disabled" @click="showShareModal = true">
<div class="flex flex-row items-center w-full gap-x-1">
<NcButton
:size="isMobileMode ? 'medium' : 'small'"
class="z-10 !rounded-lg"
:class="{
'!px-2': !isMobileMode,
'!px-0 !max-w-8.5 !min-w-8.5': isMobileMode,
}"
type="primary"
:disabled="disabled"
@click="showShareModal = true"
>
<div v-if="!isMobileMode" class="flex flex-row items-center w-full gap-x-1">
<MaterialSymbolsPublic v-if="visibility === 'public'" class="h-3.5" />
<MaterialSymbolsLockOutline v-else-if="visibility === 'private'" class="h-3.5" />
<div class="flex">{{ $t('activity.share') }}</div>
</div>
<GeneralIcon v-else icon="mobileShare" />
</NcButton>
</div>

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

@ -137,12 +137,20 @@ useEventListener(NcButton, 'mousedown', () => {
}
.nc-button {
@apply !xs:(outline-none)
box-shadow: 0px 5px 3px -2px rgba(0, 0, 0, 0.02), 0px 3px 1px -2px rgba(0, 0, 0, 0.06);
outline: none;
}
.nc-button.ant-btn.focused {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
.desktop {
.nc-button.ant-btn.focused {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
.nc-button.ant-btn-text.focused {
@apply text-brand-500;
}
}
.nc-button.ant-btn {
@ -154,7 +162,7 @@ useEventListener(NcButton, 'mousedown', () => {
}
.nc-button.ant-btn.medium {
@apply py-2 px-4 h-10 min-w-10;
@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.xsmall {
@ -167,7 +175,7 @@ useEventListener(NcButton, 'mousedown', () => {
.nc-button.ant-btn[disabled] {
box-shadow: none !important;
@apply bg-gray-50 hover:bg-gray-50 border-0 text-gray-300 cursor-not-allowed;
@apply bg-gray-50 border-0 text-gray-300 cursor-not-allowed md:(hover:bg-gray-50);
}
.nc-button.ant-btn-text.ant-btn[disabled] {
@ -179,27 +187,15 @@ useEventListener(NcButton, 'mousedown', () => {
}
.nc-button.ant-btn-primary {
@apply bg-brand-500 border-0 text-white;
&:hover {
@apply bg-brand-600 border-0;
}
@apply bg-brand-500 border-0 text-white xs:(hover:border-0) md:(hover:bg-brand-600);
}
.nc-button.ant-btn-secondary {
@apply bg-white border-1 border-gray-200 text-gray-700;
&:hover {
@apply bg-gray-100;
}
@apply bg-white border-1 border-gray-200 text-gray-700 md:(hover:bg-gray-100);
}
.nc-button.ant-btn-danger {
@apply bg-red-500 border-0;
&:hover {
@apply bg-red-600 border-0;
}
@apply bg-red-500 border-0 hover:border-0 md:(hover:bg-red-600);
}
.nc-button.ant-btn-text {
@ -211,8 +207,4 @@ useEventListener(NcButton, 'mousedown', () => {
box-shadow: none;
}
}
.nc-button.ant-btn-text.focused {
@apply text-brand-500;
}
</style>

816
packages/nc-gui/components/smartsheet/Form.vue

@ -36,7 +36,7 @@ const hiddenCols = ['created_at', 'updated_at']
const hiddenColTypes = [UITypes.Rollup, UITypes.Lookup, UITypes.Formula, UITypes.QrCode, UITypes.Barcode, UITypes.SpecificDBType]
const state = useGlobal()
const { isMobileMode, user } = useGlobal()
const formRef = ref()
@ -281,7 +281,7 @@ function setFormData() {
data = JSON.parse(formViewData.value?.email || '') || {}
} catch (e) {}
emailMe.value = data[state.user.value?.email as string]
emailMe.value = data[user.value?.email as string]
localColumns.value = col
.filter((f) => f.show && !hiddenColTypes.includes(f.uidt))
@ -314,7 +314,7 @@ async function updateEmail() {
if (!(await checkSMTPStatus())) return
const data = formViewData.value?.email ? JSON.parse(formViewData.value?.email) : {}
data[state.user.value?.email as string] = emailMe.value
data[user.value?.email as string] = emailMe.value
formViewData.value!.email = JSON.stringify(data)
} catch (e) {}
}
@ -387,458 +387,480 @@ watch(view, (nextView) => {
</script>
<template>
<a-row v-if="submitted" class="h-full" data-testid="nc-form-wrapper-submit">
<a-col :span="24">
<div v-if="formViewData" class="items-center justify-center text-center mt-2">
<a-alert type="success">
<template #message>
<div class="text-center">{{ formViewData.success_msg || 'Successfully submitted form data' }}</div>
</template>
</a-alert>
<template v-if="isMobileMode">
<div class="pl-6 pr-[120px] py-6 bg-white flex-col justify-start items-start gap-2.5 inline-flex">
<div class="text-gray-500 text-5xl font-semibold leading-16">Available<br />in Desktop</div>
<div class="text-gray-500 text-base font-medium leading-normal">Form View is currently not supported on mobile.</div>
</div>
</template>
<template v-else>
<a-row v-if="submitted" class="h-full" data-testid="nc-form-wrapper-submit">
<a-col :span="24">
<div v-if="formViewData" class="items-center justify-center text-center mt-2">
<a-alert type="success">
<template #message>
<div class="text-center">{{ formViewData.success_msg || 'Successfully submitted form data' }}</div>
</template>
</a-alert>
<div v-if="formViewData.show_blank_form" class="text-gray-400 mt-4">
New form will be loaded after {{ secondsRemain }} seconds
</div>
<div v-if="formViewData.show_blank_form" class="text-gray-400 mt-4">
New form will be loaded after {{ secondsRemain }} seconds
</div>
<div v-if="formViewData.submit_another_form || !isPublic" class="text-center mt-4">
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form</a-button>
</div>
</div>
</a-col>
</a-row>
<a-row v-else class="h-full flex" data-testid="nc-form-wrapper">
<a-col v-if="isEditable" :span="8" class="shadow p-2 md:p-4 h-full overflow-auto scrollbar-thin-dull nc-form-left-drawer">
<div class="flex flex-wrap gap-2">
<div class="flex-1 text-lg">
{{ $t('objects.fields') }}
<div v-if="formViewData.submit_another_form || !isPublic" class="text-center mt-4">
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form</a-button>
</div>
</div>
</a-col>
</a-row>
<a-row v-else class="h-full flex" data-testid="nc-form-wrapper">
<a-col v-if="isEditable" :span="8" class="shadow p-2 md:p-4 h-full overflow-auto scrollbar-thin-dull nc-form-left-drawer">
<div class="flex flex-wrap gap-2">
<div class="flex-1 text-lg">
{{ $t('objects.fields') }}
</div>
<div class="flex flex-wrap gap-2 mb-4">
<button
v-if="hiddenColumns.length"
type="button"
class="nc-form-add-all color-transition bg-white transform hover:(text-primary ring ring-accent ring-opacity-100) active:translate-y-[1px] px-2 py-1 shadow-md rounded"
data-testid="nc-form-add-all"
@click="addAllColumns"
>
<!-- Add all -->
{{ $t('general.addAll') }}
</button>
<button
v-if="localColumns.length"
type="button"
class="nc-form-remove-all color-transition bg-white transform hover:(text-primary ring ring-accent ring-opacity-100) active:translate-y-[1px] px-2 py-1 shadow-md rounded"
data-testid="nc-form-remove-all"
@click="removeAllColumns"
>
<!-- Remove all -->
{{ $t('general.removeAll') }}
</button>
<div class="flex flex-wrap gap-2 mb-4">
<button
v-if="hiddenColumns.length"
type="button"
class="nc-form-add-all color-transition bg-white transform hover:(text-primary ring ring-accent ring-opacity-100) active:translate-y-[1px] px-2 py-1 shadow-md rounded"
data-testid="nc-form-add-all"
@click="addAllColumns"
>
<!-- Add all -->
{{ $t('general.addAll') }}
</button>
<button
v-if="localColumns.length"
type="button"
class="nc-form-remove-all color-transition bg-white transform hover:(text-primary ring ring-accent ring-opacity-100) active:translate-y-[1px] px-2 py-1 shadow-md rounded"
data-testid="nc-form-remove-all"
@click="removeAllColumns"
>
<!-- Remove all -->
{{ $t('general.removeAll') }}
</button>
</div>
</div>
</div>
<Draggable
:list="hiddenColumns"
item-key="id"
draggable=".item"
group="form-inputs"
class="flex flex-col gap-2"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element, index }">
<a-card
size="small"
class="!border-0 color-transition cursor-pointer item hover:(bg-primary ring-1 ring-accent ring-opacity-100) bg-opacity-10 !rounded !shadow-lg"
:data-testid="`nc-form-hidden-column-${element.label || element.title}`"
@mousedown="moved = false"
@mousemove="moved = false"
@mouseup="handleMouseUp(element, index)"
>
<div class="flex">
<div class="flex flex-1">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<LazySmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<Draggable
:list="hiddenColumns"
item-key="id"
draggable=".item"
group="form-inputs"
class="flex flex-col gap-2"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element, index }">
<a-card
size="small"
class="!border-0 color-transition cursor-pointer item hover:(bg-primary ring-1 ring-accent ring-opacity-100) bg-opacity-10 !rounded !shadow-lg"
:data-testid="`nc-form-hidden-column-${element.label || element.title}`"
@mousedown="moved = false"
@mousemove="moved = false"
@mouseup="handleMouseUp(element, index)"
>
<div class="flex">
<div class="flex flex-1">
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<LazySmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
</div>
</div>
</a-card>
</template>
<template #footer>
<div
class="my-4 select-none border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide"
data-testid="nc-drag-n-drop-to-hide"
>
<!-- Drag and drop fields here to hide -->
{{ $t('msg.info.dragDropHide') }}
</div>
</a-card>
</template>
<template #footer>
<div
class="my-4 select-none border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center nc-drag-n-drop-to-hide"
data-testid="nc-drag-n-drop-to-hide"
>
<!-- Drag and drop fields here to hide -->
{{ $t('msg.info.dragDropHide') }}
</div>
<a-dropdown v-model:visible="showColumnDropdown" :trigger="['click']" overlay-class-name="nc-dropdown-form-add-column">
<button type="button" class="group w-full mt-2" @click.stop="showColumnDropdown = true">
<span class="flex items-center flex-wrap justify-center gap-2 prose-sm text-gray-400">
<component :is="iconMap.plus" class="color-transition transform group-hover:(text-accent scale-125)" />
<!-- Add new field to this table -->
<span class="color-transition group-hover:text-primary break-words">
{{ $t('activity.addField') }}
<a-dropdown
v-model:visible="showColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-form-add-column"
>
<button type="button" class="group w-full mt-2" @click.stop="showColumnDropdown = true">
<span class="flex items-center flex-wrap justify-center gap-2 prose-sm text-gray-400">
<component :is="iconMap.plus" class="color-transition transform group-hover:(text-accent scale-125)" />
<!-- Add new field to this table -->
<span class="color-transition group-hover:text-primary break-words">
{{ $t('activity.addField') }}
</span>
</span>
</span>
</button>
</button>
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="showColumnDropdown"
@submit="submitCallback"
@cancel="showColumnDropdown = false"
@click.stop
@keydown.stop
/>
</template>
</a-dropdown>
</template>
</Draggable>
</a-col>
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-dull">
<div class="h-[200px] bg-primary bg-opacity-75">
<!-- for future implementation of cover image -->
</div>
<a-card
class="p-4 border-none"
:body-style="{
maxWidth: 'max(50vw, 700px)',
margin: '0 auto',
marginTop: '-200px',
padding: '0px',
}"
>
<a-form ref="formRef" :model="formState" class="nc-form" no-style>
<a-card class="!rounded !shadow !m-2 md:(!m-4) xl:(!m-8)" :body-style="{ paddingLeft: '0px', paddingRight: '0px' }">
<!-- Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item v-if="isEditable">
<a-textarea
v-model:value="formViewData.heading"
class="w-full !font-bold !text-4xl !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{ 'borderRightWidth': '0px !important', 'height': '54px', 'min-height': '54px', 'resize': 'vertical' }"
size="large"
hide-details
placeholder="Form Title"
:bordered="false"
data-testid="nc-form-heading"
@blur="updateView"
@keydown.enter="updateView"
/>
</a-form-item>
</div>
<div v-else class="px-4 ml-3 w-full text-bold text-4xl">{{ formViewData.heading }}</div>
<!-- Sub Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item>
<a-textarea
v-model:value="formViewData.subheading"
class="w-full !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{ 'borderRightWidth': '0px !important', 'height': '40px', 'min-height': '40px', 'resize': 'vertical' }"
size="large"
hide-details
:placeholder="$t('msg.info.formDesc')"
:bordered="false"
:disabled="!isEditable"
data-testid="nc-form-sub-heading"
@blur="updateView"
@click="updateView"
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="showColumnDropdown"
@submit="submitCallback"
@cancel="showColumnDropdown = false"
@click.stop
@keydown.stop
/>
</a-form-item>
</div>
</template>
</a-dropdown>
</template>
</Draggable>
</a-col>
<div v-else class="px-4 ml-3 w-full text-bold text-md">{{ formViewData.subheading || '---' }}</div>
<Draggable
ref="draggableRef"
:list="localColumns"
item-key="fk_column_id"
draggable=".item"
group="form-inputs"
class="h-full"
:move="onMoveCallback"
@change="onMove($event)"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element, index }">
<div
class="color-transition nc-editable item cursor-pointer hover:(bg-primary bg-opacity-10 ring-1 ring-accent ring-opacity-100) px-4 lg:px-12 py-4 relative"
:class="[
`nc-form-drag-${element.title.replaceAll(' ', '')}`,
{
'bg-primary bg-opacity-5 ring-0.5 ring-accent ring-opacity-100': activeRow === element.title,
},
]"
data-testid="nc-form-fields"
@click="activeRow = element.title"
>
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-dull">
<div class="h-[200px] bg-primary bg-opacity-75">
<!-- for future implementation of cover image -->
</div>
<a-card
class="p-4 border-none"
:body-style="{
maxWidth: 'max(50vw, 700px)',
margin: '0 auto',
marginTop: '-200px',
padding: '0px',
}"
>
<a-form ref="formRef" :model="formState" class="nc-form" no-style>
<a-card class="!rounded !shadow !m-2 md:(!m-4) xl:(!m-8)" :body-style="{ paddingLeft: '0px', paddingRight: '0px' }">
<!-- Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item v-if="isEditable">
<a-textarea
v-model:value="formViewData.heading"
class="w-full !font-bold !text-4xl !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{
'borderRightWidth': '0px !important',
'height': '54px',
'min-height': '54px',
'resize': 'vertical',
}"
size="large"
hide-details
placeholder="Form Title"
:bordered="false"
data-testid="nc-form-heading"
@blur="updateView"
@keydown.enter="updateView"
/>
</a-form-item>
</div>
<div v-else class="px-4 ml-3 w-full text-bold text-4xl">{{ formViewData.heading }}</div>
<!-- Sub Header -->
<div v-if="isEditable" class="px-4 lg:px-12">
<a-form-item>
<a-textarea
v-model:value="formViewData.subheading"
class="w-full !border-0 !border-b-1 !border-dashed !rounded-none !border-gray-400"
:style="{
'borderRightWidth': '0px !important',
'height': '40px',
'min-height': '40px',
'resize': 'vertical',
}"
size="large"
hide-details
:placeholder="$t('msg.info.formDesc')"
:bordered="false"
:disabled="!isEditable"
data-testid="nc-form-sub-heading"
@blur="updateView"
@click="updateView"
/>
</a-form-item>
</div>
<div v-else class="px-4 ml-3 w-full text-bold text-md">{{ formViewData.subheading || '---' }}</div>
<Draggable
ref="draggableRef"
:list="localColumns"
item-key="fk_column_id"
draggable=".item"
group="form-inputs"
class="h-full"
:move="onMoveCallback"
@change="onMove($event)"
@start="drag = true"
@end="drag = false"
>
<template #item="{ element, index }">
<div
v-if="isUIAllowed('viewFieldEdit') && !isRequired(element, element.required)"
class="absolute flex top-2 right-2"
class="color-transition nc-editable item cursor-pointer hover:(bg-primary bg-opacity-10 ring-1 ring-accent ring-opacity-100) px-4 lg:px-12 py-4 relative"
:class="[
`nc-form-drag-${element.title.replaceAll(' ', '')}`,
{
'bg-primary bg-opacity-5 ring-0.5 ring-accent ring-opacity-100': activeRow === element.title,
},
]"
data-testid="nc-form-fields"
@click="activeRow = element.title"
>
<component
:is="iconMap.eyeSlash"
class="opacity-0 nc-field-remove-icon"
data-testid="nc-field-remove-icon"
@click.stop="hideColumn(index)"
/>
</div>
<div v-if="activeRow === element.title" class="flex flex-col gap-3 mb-3">
<div class="flex gap-2 items-center">
<span
class="text-gray-500 mr-2 nc-form-input-required"
data-testid="nc-form-input-required"
@click="
() => {
element.required = !element.required
updateColMeta(element)
}
"
>
{{ $t('general.required') }}
</span>
<a-switch
v-model:checked="element.required"
v-e="['a:form-view:field:mark-required']"
size="small"
@change="updateColMeta(element)"
<div
v-if="isUIAllowed('viewFieldEdit') && !isRequired(element, element.required)"
class="absolute flex top-2 right-2"
>
<component
:is="iconMap.eyeSlash"
class="opacity-0 nc-field-remove-icon"
data-testid="nc-field-remove-icon"
@click.stop="hideColumn(index)"
/>
</div>
<a-form-item v-if="columnSupportsScanning(element.uidt)" class="my-0 w-1/2 !mb-1">
<div v-if="activeRow === element.title" class="flex flex-col gap-3 mb-3">
<div class="flex gap-2 items-center">
<span
class="text-gray-500 mr-2 nc-form-input-required"
data-testid="nc-form-input-enable-scanner"
data-testid="nc-form-input-required"
@click="
() => {
element.general.enable_scanner = !element.general.enable_scanner
element.required = !element.required
updateColMeta(element)
}
"
>
{{ $t('general.enableScanner') }}
{{ $t('general.required') }}
</span>
<a-switch
v-model:checked="element.enable_scanner"
v-e="['a:form-view:field:mark-enable-scaner']"
v-model:checked="element.required"
v-e="['a:form-view:field:mark-required']"
size="small"
@change="updateColMeta(element)"
/>
</div>
</a-form-item>
<a-form-item class="my-0 w-1/2 !mb-1">
<a-input
v-model:value="element.label"
type="text"
class="form-meta-input nc-form-input-label"
<a-form-item v-if="columnSupportsScanning(element.uidt)" class="my-0 w-1/2 !mb-1">
<div class="flex gap-2 items-center">
<span
class="text-gray-500 mr-2 nc-form-input-required"
data-testid="nc-form-input-enable-scanner"
@click="
() => {
element.general.enable_scanner = !element.general.enable_scanner
updateColMeta(element)
}
"
>
{{ $t('general.enableScanner') }}
</span>
<a-switch
v-model:checked="element.enable_scanner"
v-e="['a:form-view:field:mark-enable-scaner']"
size="small"
@change="updateColMeta(element)"
/>
</div>
</a-form-item>
<a-form-item class="my-0 w-1/2 !mb-1">
<a-input
v-model:value="element.label"
type="text"
class="form-meta-input nc-form-input-label"
data-testid="nc-form-input-label"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
>
</a-input>
</a-form-item>
<a-form-item class="mt-2 mb-0 w-1/2 !mb-1">
<a-input
v-model:value="element.description"
type="text"
class="form-meta-input text-sm nc-form-input-help-text"
data-testid="nc-form-input-help-text"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
/>
</a-form-item>
</div>
<div>
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
data-testid="nc-form-input-label"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
>
</a-input>
</a-form-item>
/>
<a-form-item class="mt-2 mb-0 w-1/2 !mb-1">
<a-input
v-model:value="element.description"
type="text"
class="form-meta-input text-sm nc-form-input-help-text"
data-testid="nc-form-input-help-text"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
<LazySmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
data-testid="nc-form-input-label"
/>
</a-form-item>
</div>
</div>
<div>
<LazySmartsheetHeaderVirtualCell
<a-form-item
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
data-testid="nc-form-input-label"
/>
<LazySmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
data-testid="nc-form-input-label"
/>
</div>
<a-form-item
v-if="isVirtualCol(element)"
:name="element.title"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
message: `${element.label || element.title} is required`,
},
]"
>
<LazySmartsheetVirtualCell
v-model="formState[element.title]"
:row="row"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
@click.stop.prevent
/>
</a-form-item>
<a-form-item
v-else
:name="element.title"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
message: `${element.label || element.title} is required`,
},
]"
>
<LazySmartsheetDivDataCell class="relative">
<LazySmartsheetCell
:name="element.title"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
message: `${element.label || element.title} is required`,
},
]"
>
<LazySmartsheetVirtualCell
v-model="formState[element.title]"
class="nc-input truncate"
:row="row"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
:edit-enabled="editEnabled[index]"
@click="editEnabled[index] = true"
@cancel="editEnabled[index] = false"
@update:edit-enabled="editEnabled[index] = $event"
@click.stop.prevent
/>
</LazySmartsheetDivDataCell>
</a-form-item>
</a-form-item>
<div class="nc-form-help-text text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">
{{ element.description }}
<a-form-item
v-else
:name="element.title"
class="!mb-0 nc-input-required-error"
:rules="[
{
required: isRequired(element, element.required),
message: `${element.label || element.title} is required`,
},
]"
>
<LazySmartsheetDivDataCell class="relative">
<LazySmartsheetCell
v-model="formState[element.title]"
class="nc-input truncate"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:data-testid="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
:edit-enabled="editEnabled[index]"
@click="editEnabled[index] = true"
@cancel="editEnabled[index] = false"
@update:edit-enabled="editEnabled[index] = $event"
@click.stop.prevent
/>
</LazySmartsheetDivDataCell>
</a-form-item>
<div class="nc-form-help-text text-gray-500 text-xs" data-testid="nc-form-input-help-text-label">
{{ element.description }}
</div>
</div>
</div>
</template>
</template>
<template #footer>
<div
v-if="!localColumns.length"
class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center"
<template #footer>
<div
v-if="!localColumns.length"
class="mt-4 border-dashed border-2 border-gray-400 py-3 text-gray-400 text-center"
>
Drag and drop fields here to add
</div>
</template>
</Draggable>
<div class="justify-center flex mt-6">
<button
type="submit"
:disabled="!isUIAllowed('dataInsert')"
class="uppercase scaling-btn nc-form-submit"
data-testid="nc-form-submit"
@click="submitForm"
>
Drag and drop fields here to add
</div>
</template>
</Draggable>
<div class="justify-center flex mt-6">
<button
type="submit"
:disabled="!isUIAllowed('dataInsert')"
class="uppercase scaling-btn nc-form-submit"
data-testid="nc-form-submit"
@click="submitForm"
>
{{ $t('general.submit') }}
</button>
{{ $t('general.submit') }}
</button>
</div>
</a-card>
</a-form>
<a-divider />
<div v-if="isEditable" class="px-4 flex flex-col gap-2">
<!-- After form is submitted -->
<div class="text-lg text-gray-700">
{{ $t('msg.info.afterFormSubmitted') }}
</div>
</a-card>
</a-form>
<a-divider />
<!-- Show this message -->
<div class="text-gray-500 text-bold">{{ $t('msg.info.showMessage') }}:</div>
<a-textarea
v-model:value="formViewData.success_msg"
:rows="3"
hide-details
class="nc-form-after-submit-msg"
data-testid="nc-form-after-submit-msg"
@change="updateView"
/>
<!-- Other options -->
<div class="flex flex-col gap-2 mt-4">
<div class="flex items-center">
<!-- Show "Submit Another Form" button -->
<a-switch
v-model:checked="formViewData.submit_another_form"
v-e="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
data-testid="nc-form-checkbox-submit-another-form"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
</div>
<div v-if="isEditable" class="px-4 flex flex-col gap-2">
<!-- After form is submitted -->
<div class="text-lg text-gray-700">
{{ $t('msg.info.afterFormSubmitted') }}
</div>
<div class="flex items-center">
<!-- Show a blank form after 5 seconds -->
<a-switch
v-model:checked="formViewData.show_blank_form"
v-e="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
data-testid="nc-form-checkbox-show-blank-form"
@change="updateView"
/>
<!-- Show this message -->
<div class="text-gray-500 text-bold">{{ $t('msg.info.showMessage') }}:</div>
<a-textarea
v-model:value="formViewData.success_msg"
:rows="3"
hide-details
class="nc-form-after-submit-msg"
data-testid="nc-form-after-submit-msg"
@change="updateView"
/>
<!-- Other options -->
<div class="flex flex-col gap-2 mt-4">
<div class="flex items-center">
<!-- Show "Submit Another Form" button -->
<a-switch
v-model:checked="formViewData.submit_another_form"
v-e="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
data-testid="nc-form-checkbox-submit-another-form"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
</div>
<span class="ml-4">{{ $t('msg.info.showBlankForm') }}</span>
</div>
<div class="flex items-center">
<!-- Show a blank form after 5 seconds -->
<a-switch
v-model:checked="formViewData.show_blank_form"
v-e="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
data-testid="nc-form-checkbox-show-blank-form"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.showBlankForm') }}</span>
</div>
<div class="mb-12 flex items-center">
<a-switch
v-model:checked="emailMe"
v-e="[`a:form-view:email-me`]"
size="small"
class="nc-form-checkbox-send-email"
data-testid="nc-form-checkbox-send-email"
@change="onEmailChange"
/>
<div class="mb-12 flex items-center">
<a-switch
v-model:checked="emailMe"
v-e="[`a:form-view:email-me`]"
size="small"
class="nc-form-checkbox-send-email"
data-testid="nc-form-checkbox-send-email"
@change="onEmailChange"
/>
<!-- Email me at <email> -->
<span class="ml-4">
{{ $t('msg.info.emailForm') }} <span class="text-bold text-gray-600">{{ state.user.value?.email }}</span>
</span>
<!-- Email me at <email> -->
<span class="ml-4">
{{ $t('msg.info.emailForm') }} <span class="text-bold text-gray-600">{{ user?.email }}</span>
</span>
</div>
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</template>
</template>
<style scoped lang="scss">

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

@ -23,6 +23,8 @@ const { locale } = useI18n()
const vPaginationData = useVModel(props, 'paginationData', emits)
const { isMobileMode } = useGlobal()
const { alignCountOnRight, customLabel, changePage } = props
const fixedSize = toRef(props, 'fixedSize')
@ -56,8 +58,8 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
<template>
<div
class="flex items-center bg-white border-gray-200 h-10 nc-pagination-wrapper"
:class="{ 'border-t-1': !isGroupBy }"
class="flex items-center bg-white border-gray-200 nc-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;' : ''
}${extraStyle}`"
@ -94,7 +96,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</a-input>
</div>
</template>
<div class="flex-1 flex justify-end items-center">
<div v-if="!isMobileMode" class="flex-1 flex justify-end items-center">
<GeneralApiTiming v-if="isEeUI && props.showApiTiming" class="m-1" />
<div class="text-right">
<span

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

@ -12,22 +12,11 @@ const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const { allowCSVDownload } = useSharedView()
const isViewSidebarAvailable = computed(
() => (isGrid.value || isGallery.value || isKanban.value || isMap.value) && !isPublic.value,
)
</script>
<template>
<div
class="nc-table-toolbar h-12 min-h-12 py-1 flex gap-2 items-center border-b border-gray-200 overflow-hidden"
:class="{
'nc-table-toolbar-mobile': isMobileMode,
'max-h-[var(--topbar-height)] min-h-[var(--topbar-height)]': !isMobileMode,
'pl-3 pr-0': isViewSidebarAvailable,
'px-3': !isViewSidebarAvailable,
}"
style="z-index: 7"
class="nc-table-toolbar py-1 px-1 xs:(px-1) flex gap-2 items-center border-b border-gray-200 overflow-hidden xs:(min-h-14) max-h-[var(--topbar-height)] min-h-[var(--topbar-height)] z-7"
>
<template v-if="isViewsLoading">
<a-skeleton-input :active="true" class="!w-44 !h-4 ml-2 !rounded overflow-hidden" />
@ -45,20 +34,30 @@ const isViewSidebarAvailable = computed(
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarRowHeight v-if="isGrid" />
<LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" />
<template v-if="!isMobileMode">
<LazySmartsheetToolbarRowHeight v-if="isGrid" />
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />
<!-- <LazySmartsheetToolbarQrScannerButton v-if="isMobileMode && (isGrid || isKanban || isGallery)" /> -->
<div v-if="!isMobileMode" class="flex-1" />
<LazySmartsheetToolbarExport v-if="(!isPublic && !isUIAllowed('dataInsert')) || (isPublic && allowCSVDownload)" />
<LazySmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink" />
<div class="flex-1" />
</template>
<LazySmartsheetToolbarViewActions
v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false"
<LazySmartsheetToolbarSearchData
v-if="(isGrid || isGallery || isKanban) && !isPublic"
:class="{
'shrink': !isMobileMode,
'w-full': isMobileMode,
}"
/>
<template v-if="!isMobileMode">
<LazySmartsheetToolbarViewActions
v-if="(isGrid || isGallery || isKanban || isMap) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false"
/>
</template>
<LazySmartsheetToolbarOpenViewSidebarBtn v-if="isViewSidebarAvailable" />
</template>
</div>

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

@ -10,18 +10,14 @@ const isPublic = inject(IsPublicInj, ref(false))
const { isViewsLoading } = storeToRefs(useViewsStore())
const { isMobileMode } = useGlobal()
const { isMobileMode } = storeToRefs(useConfigStore())
const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
</script>
<template>
<div
class="nc-table-topbar h-20 py-1 flex gap-2 items-center pr-2 pl-2.5 border-b border-gray-200 overflow-hidden relative"
:class="{
'nc-table-toolbar-mobile': isMobileMode,
'max-h-[var(--topbar-height)] min-h-[var(--topbar-height)]': !isMobileMode,
}"
class="nc-table-topbar h-20 py-1 flex gap-2 items-center border-b border-gray-200 overflow-hidden relative max-h-[var(--topbar-height)] min-h-[var(--topbar-height)] md:(pr-2 pl-2.5) xs:(px-1)"
style="z-index: 7"
>
<template v-if="isViewsLoading">
@ -31,13 +27,18 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<GeneralOpenLeftSidebarBtn />
<LazySmartsheetToolbarViewInfo v-if="!isPublic" />
<div v-if="!isMobileMode" class="flex-1" />
<div class="flex-1" />
<div v-if="!isSharedBase" class="absolute mx-auto -left-1/8 right-0 w-47.5"><SmartsheetTopbarSelectMode /></div>
<div v-if="!isSharedBase && !isMobileMode" class="absolute mx-auto -left-1/8 right-0 w-47.5">
<SmartsheetTopbarSelectMode />
</div>
<GeneralApiLoader />
<GeneralApiLoader v-if="!isMobileMode" />
<LazyGeneralShareProject v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic" is-view-toolbar />
<LazyGeneralShareProject
v-if="(isForm || isGrid || isKanban || isGallery || isMap) && !isPublic && !isMobileMode"
is-view-toolbar
/>
<LazyGeneralLanguage
v-if="isSharedBase"

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

@ -121,6 +121,10 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isMobileMode } = useGlobal()
const scrollParent = inject(ScrollParentInj, ref<undefined>())
const { isPkAvail, isSqlView, eventBus } = useSmartsheetStoreOrThrow()
@ -1194,14 +1198,18 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
@xcresized="resizingCol = null"
>
<div class="w-full h-full flex items-center">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)"
:column="col"
:hide-menu="readOnly || isMobileMode"
/>
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly || isMobileMode" />
</div>
</th>
<th
v-if="isAddingColumnAllowed"
v-e="['c:column:add']"
class="cursor-pointer !border-0 relative"
class="cursor-pointer !border-0 relative !xs:hidden"
:style="{
borderWidth: '0px !important',
}"
@ -1320,7 +1328,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
<LazySmartsheetRow v-for="(row, rowIndex) of dataRef" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr
class="nc-grid-row"
class="nc-grid-row !xs:h-14"
:style="{ height: rowHeight ? `${rowHeight * 1.8}rem` : `1.8rem` }"
:data-testid="`grid-row-${rowIndex}`"
>
@ -1452,7 +1460,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
<tr
v-if="isAddingEmptyRowAllowed && !isGroupBy"
v-e="['c:row:add:grid-bottom']"
class="text-left nc-grid-add-new-cell cursor-pointer group relative z-3"
class="text-left nc-grid-add-new-cell cursor-pointer group relative z-3 xs:hidden"
:class="{
'!border-r-2 !border-r-gray-100': visibleColLength === 1,
}"
@ -1583,6 +1591,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
</div>
<LazySmartsheetPagination
v-else-if="headerOnly !== true"
:key="isMobileMode"
v-model:pagination-data="paginationDataRef"
show-api-timing
align-count-on-right
@ -1593,7 +1602,11 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
>
<template #add-record>
<div v-if="isAddingEmptyRowAllowed" class="flex ml-1">
<NcButton v-if="isMobileMode" class="nc-grid-add-new-row" type="secondary" @click="onNewRecordToFormClick()">
{{ $t('activity.newRecord') }}
</NcButton>
<a-dropdown-button
v-else
class="nc-grid-add-new-row"
placement="top"
@click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"

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

@ -54,7 +54,12 @@ useMenuCloseOnEsc(open)
</script>
<template>
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu nc-toolbar-dropdown">
<NcDropdown
v-model:visible="open"
:trigger="['click']"
overlay-class-name="nc-dropdown-filter-menu nc-toolbar-dropdown"
class="!xs:hidden"
>
<div :class="{ 'nc-active-btn': filtersLength }">
<a-button v-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<div class="flex items-center gap-2">

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

@ -286,7 +286,12 @@ useMenuCloseOnEsc(open)
</script>
<template>
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu nc-toolbar-dropdown">
<NcDropdown
v-model:visible="open"
:trigger="['click']"
overlay-class-name="nc-dropdown-fields-menu nc-toolbar-dropdown"
class="!xs:hidden"
>
<div :class="{ 'nc-active-btn': numberOfHiddenFields }">
<a-button v-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">

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

@ -151,6 +151,7 @@ watch(open, () => {
v-model:visible="open"
offset-y
:trigger="['click']"
class="!xs:hidden"
overlay-class-name="nc-dropdown-group-by-menu nc-toolbar-dropdown"
>
<div :class="{ 'nc-active-btn': groupedByColumnIds?.length }">

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

@ -76,7 +76,7 @@ const handleChange = () => {
</script>
<template>
<a-dropdown v-if="!IsPublic" v-model:visible="mappedByDropdown" :trigger="['click']">
<a-dropdown v-if="!IsPublic" v-model:visible="mappedByDropdown" :trigger="['click']" class="!xs:hidden">
<div class="nc-map-btn">
<a-button v-e="['c:map:change-grouping-field']" class="nc-map-stacked-by-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-1">

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

@ -23,6 +23,8 @@ const { search, loadFieldQuery } = useFieldQuery()
const isDropdownOpen = ref(false)
const { isMobileMode } = useGlobal()
const isFocused = ref(false)
const searchDropdown = ref(null)
@ -83,7 +85,7 @@ watch(columns, () => {
<template>
<div
class="flex flex-row border-1 rounded-lg h-8 ml-1 border-gray-200 overflow-hidden"
class="flex flex-row border-1 rounded-lg h-8 xs:(h-10 ml-0) ml-1 border-gray-200 overflow-hidden"
:class="{ 'border-primary': search.query.length !== 0 || isFocused }"
>
<div
@ -93,11 +95,23 @@ watch(columns, () => {
@click="isDropdownOpen = !isDropdownOpen"
>
<GeneralIcon icon="search" class="ml-1 mr-2 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div class="w-16 group-hover:w-12 text-[0.75rem] font-medium text-gray-400 truncate">
<div v-if="!isMobileMode" class="w-16 group-hover:w-12 text-[0.75rem] font-medium text-gray-400 truncate">
{{ displayColumnLabel }}
</div>
<div class="hidden group-hover:block">
<component :is="iconMap.arrowDown" class="text-gray-400 text-sm" />
<div
:class="{
'hidden group-hover:block': !isMobileMode,
'text-gray-700': isMobileMode,
}"
>
<component
:is="iconMap.arrowDown"
class="text-sm"
:class="{
'text-gray-400': !isMobileMode,
'text-gray-600': isMobileMode,
}"
/>
</div>
<a-select
v-model:value="search.field"
@ -120,10 +134,7 @@ watch(columns, () => {
<a-input
v-model:value="search.query"
size="small"
class="text-xs"
:style="{
width: '10rem',
}"
class="text-xs w-40"
:placeholder="`${$t('general.search')} in ${columns?.find((column) => column.value === search.field)?.label}`"
:bordered="false"
data-testid="search-data-input"

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

@ -106,7 +106,12 @@ watch(open, () => {
</script>
<template>
<NcDropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-sort-menu nc-toolbar-dropdown">
<NcDropdown
v-model:visible="open"
:trigger="['click']"
class="!xs:hidden"
overlay-class-name="nc-dropdown-sort-menu nc-toolbar-dropdown"
>
<div :class="{ 'nc-active-btn': sorts?.length }">
<a-button v-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-2">

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

@ -110,6 +110,7 @@ const handleChange = () => {
v-model:visible="open"
:trigger="['click']"
overlay-class-name="nc-dropdown-kanban-stacked-by-menu"
class="!xs:hidden"
>
<div class="nc-kanban-btn">
<a-button

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

@ -1,6 +1,8 @@
<script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { activeTable } = storeToRefs(useTablesStore())
@ -10,33 +12,54 @@ const { activeTable } = storeToRefs(useTablesStore())
<div
class="flex flex-row font-medium items-center border-gray-50 mt-0.5"
:class="{
'min-w-2/5 max-w-2/5': activeView?.type !== ViewTypes.KANBAN,
'min-w-1/4 max-w-1/4': activeView?.type === ViewTypes.KANBAN,
'min-w-2/5 max-w-2/5': !isMobileMode && activeView?.type !== ViewTypes.KANBAN,
'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN,
'w-2/3 text-base ml-1.5': isMobileMode,
}"
>
<LazyGeneralEmojiPicker :emoji="activeTable?.meta?.icon" readonly size="xsmall">
<template #default>
<MdiTable class="min-w-5 !text-gray-500" />
</template>
</LazyGeneralEmojiPicker>
<span
class="text-ellipsis overflow-hidden pl-1 max-w-1/2 text-gray-500"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ activeTable?.title }}
</span>
<div class="px-2 text-gray-300">/</div>
<LazyGeneralEmojiPicker :emoji="activeView?.meta?.icon" readonly size="xsmall">
<template #default>
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" />
</template>
</LazyGeneralEmojiPicker>
<span class="truncate pl-1.25 text-gray-700 max-w-28/100">
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view'" />
<template v-if="!(isMobileMode && !activeView?.is_default)">
<LazyGeneralEmojiPicker :emoji="activeTable?.meta?.icon" readonly size="xsmall">
<template #default>
<MdiTable
class="min-w-5"
:class="{
'!text-gray-500': !isMobileMode,
'!text-gray-700': isMobileMode,
}"
/>
</template>
</LazyGeneralEmojiPicker>
<span
class="text-ellipsis overflow-hidden pl-1 text-gray-500"
:class="{
'text-gray-500': !isMobileMode,
'text-gray-700 max-w-1/2': isMobileMode,
}"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ activeTable?.title }}
</span>
</template>
<div v-if="!isMobileMode" class="px-2 text-gray-300">/</div>
<template v-if="!(isMobileMode && activeView?.is_default)">
<LazyGeneralEmojiPicker :emoji="activeView?.meta?.icon" readonly size="xsmall">
<template #default>
<GeneralViewIcon :meta="{ type: activeView?.type }" class="min-w-4.5 text-lg flex" />
</template>
</LazyGeneralEmojiPicker>
<span
class="truncate pl-1.25 text-gray-700"
:class="{
'max-w-28/100': !isMobileMode,
}"
>
{{ activeView?.is_default ? $t('title.defaultView') : activeView?.title }}
</span>
</template>
<LazySmartsheetToolbarReload v-if="openedViewsTab === 'view' && !isMobileMode" />
</div>
</template>

9
packages/nc-gui/components/tabs/Smartsheet.vue

@ -47,7 +47,10 @@ const meta = computed<TableType | undefined>(() => {
return viewId && metas.value[viewId]
})
const { activeView, openedViewsTab } = storeToRefs(useViewsStore())
const { handleSidebarOpenOnMobileForNonViews } = useConfigStore()
const { activeTableId } = storeToRefs(useTablesStore())
const { activeView, openedViewsTab, activeViewTitleOrId } = storeToRefs(useViewsStore())
const { isGallery, isGrid, isForm, isKanban, isLocked, isMap } = useProvideSmartsheetStore(activeView, meta)
useSqlEditor()
@ -153,6 +156,10 @@ const onDrop = async (event: DragEvent) => {
console.log('error', e)
}
}
watch([activeViewTitleOrId, activeTableId], () => {
handleSidebarOpenOnMobileForNonViews()
})
</script>
<template>

6
packages/nc-gui/composables/useTableNew.ts

@ -54,6 +54,8 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
const tables = computed(() => projectTables.value.get(param.projectId) || [])
const project = computed(() => projects.value.get(param.projectId))
const { loadViews } = useViewsStore()
const openTable = async (table: TableType) => {
if (!table.project_id) return
@ -78,6 +80,10 @@ export function useTableNew(param: { onTableCreate?: (tableMeta: TableType) => v
projectIdOrBaseId = route.value.params.projectId as string
}
await loadViews({
tableId: table.id,
})
await navigateTo({
path: `/${workspaceIdOrType}/${projectIdOrBaseId}/${table?.id}`,
query: route.value.query,

10
packages/nc-gui/layouts/base.vue

@ -31,12 +31,12 @@ hooks.hook('page:finish', () => {
</script>
<template>
<a-layout id="nc-app" has-sider>
<a-layout id="nc-app" class="nc-app" has-sider>
<Transition name="slide">
<div v-show="hasSider" id="nc-sidebar-left" ref="sidebar" />
</Transition>
<a-layout class="!flex-col">
<a-layout class="!flex-col h-screen">
<a-layout-header v-if="!route.meta.public && signedIn && !route.meta.hideHeader" class="nc-navbar">
<div
v-if="!route.params.projectType"
@ -134,7 +134,7 @@ hooks.hook('page:finish', () => {
<LazyGeneralLanguage v-if="!signedIn && !route.params.projectId && !route.params.erdUuid" class="nc-lang-btn" />
</a-tooltip>
<div class="w-full h-full overflow-hidden">
<div class="w-full h-full overflow-hidden nc-layout-base-inner">
<slot />
</div>
</a-layout>
@ -163,4 +163,8 @@ hooks.hook('page:finish', () => {
.nc-navbar {
@apply flex !bg-white items-center !pl-2 !pr-5;
}
.nc-layout-base-inner > div {
@apply h-full;
}
</style>

2
packages/nc-gui/layouts/dashboard.vue

@ -10,7 +10,7 @@ export default {
</script>
<template>
<NuxtLayout class="h-screen">
<NuxtLayout>
<slot v-if="!route.meta.hasSidebar" name="content" />
<LazyDashboardView v-else>

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

@ -4,6 +4,8 @@ export const SYSTEM_COLUMNS = ['id', 'title', 'created_at', 'updated_at']
export const EMPTY_TITLE_PLACEHOLDER_DOCS = 'Untitled'
export const MAX_WIDTH_FOR_MOBILE_MODE = 480
export const BASE_FALLBACK_URL = process.env.NODE_ENV === 'production' ? '..' : 'http://localhost:8080'
export const GROUP_BY_VARS = {

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

@ -6,6 +6,8 @@ import { message, ref, resolveComponent, storeToRefs, useDialog, useFileDialog,
const projectStore = useProject()
const { project } = storeToRefs(projectStore)
const { isMobileMode } = useGlobal()
const { files, reset } = useFileDialog()
const { bases } = storeToRefs(projectStore)
@ -129,5 +131,5 @@ watch(
</script>
<template>
<ProjectView />
<ProjectView v-if="!isMobileMode" />
</template>

70
packages/nc-gui/store/config.ts

@ -0,0 +1,70 @@
import { acceptHMRUpdate, defineStore } from 'pinia'
import { MAX_WIDTH_FOR_MOBILE_MODE } from '~/lib'
export const useConfigStore = defineStore('configStore', () => {
const { isMobileMode: globalIsMobile } = useGlobal()
const { width } = useWindowSize()
const sidebarStore = useSidebarStore()
const viewsStore = useViewsStore()
const tablesStore = useTablesStore()
const isViewPortMobile = () => width.value < MAX_WIDTH_FOR_MOBILE_MODE
const isMobileMode = ref(isViewPortMobile())
const onViewPortResize = () => {
isMobileMode.value = isViewPortMobile()
}
window.addEventListener('DOMContentLoaded', onViewPortResize)
window.addEventListener('resize', onViewPortResize)
watch(
isMobileMode,
() => {
globalIsMobile.value = isMobileMode.value
// Change --topbar-height css variable
document.documentElement.style.setProperty('--topbar-height', isMobileMode.value ? '3.25rem' : '3.1rem')
// Set .mobile-mode class on body
if (isMobileMode.value) {
document.body.classList.add('mobile')
document.body.classList.remove('desktop')
} else {
document.body.classList.add('desktop')
document.body.classList.remove('mobile')
}
},
{
immediate: true,
},
)
const handleSidebarOpenOnMobileForNonViews = () => {
if (!isViewPortMobile()) return
if (!viewsStore.activeViewTitleOrId && !tablesStore.activeTableId) {
nextTick(() => {
sidebarStore.isLeftSidebarOpen = true
})
} else {
sidebarStore.isLeftSidebarOpen = false
}
}
watch([viewsStore.activeViewTitleOrId, tablesStore.activeTableId], () => {
handleSidebarOpenOnMobileForNonViews()
})
return {
isMobileMode,
isViewPortMobile,
handleSidebarOpenOnMobileForNonViews,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useConfigStore as any, import.meta.hot))
}

17
packages/nc-gui/store/sidebar.ts

@ -1,12 +1,17 @@
import { defineStore } from 'pinia'
import { acceptHMRUpdate, defineStore } from 'pinia'
import { MAX_WIDTH_FOR_MOBILE_MODE } from '~/lib'
export const useSidebarStore = defineStore('sidebarStore', () => {
const isLeftSidebarOpen = ref(true)
const { width } = useWindowSize()
const isViewPortMobile = () => width.value < MAX_WIDTH_FOR_MOBILE_MODE
const isLeftSidebarOpen = ref(!isViewPortMobile())
const isRightSidebarOpen = ref(true)
const leftSidebarWidthPercent = ref(20)
const leftSidebarWidthPercent = ref(isViewPortMobile() ? 0 : 20)
const leftSideBarSize = ref({
old: leftSidebarWidthPercent.value,
old: 20,
current: leftSidebarWidthPercent.value,
})
@ -33,3 +38,7 @@ export const useSidebarStore = defineStore('sidebarStore', () => {
rightSidebarState,
}
})
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useSidebarStore as any, import.meta.hot))
}

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

@ -81,9 +81,16 @@ export const useViewsStore = defineStore('viewsStore', () => {
// Used for Grid View Pagination
const isPaginationLoading = ref(false)
const loadViews = async ({ tableId, ignoreLoading }: { tableId?: string; ignoreLoading?: boolean } = {}) => {
const loadViews = async ({
tableId,
ignoreLoading,
force,
}: { tableId?: string; ignoreLoading?: boolean; force?: boolean } = {}) => {
tableId = tableId ?? tablesStore.activeTableId
if (tableId) {
if (!force && viewsByTable.value.get(tableId)) return
if (!ignoreLoading) isViewsLoading.value = true
const response = (await $api.dbView.list(tableId)).list as ViewType[]

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

@ -37,6 +37,7 @@ import NcLayers from '~icons/nc-icons/layers'
import NcUsers from '~icons/nc-icons/users'
import NcCheck from '~icons/nc-icons/check'
import PlusSquare from '~icons/nc-icons/plus-square'
import MobileShare from '~icons/nc-icons/share'
import PhLayout from '~icons/ph/layout'
import Delete from '~icons/material-symbols/delete-outline-rounded'
import CiFilter from '~icons/mdi/filter-outline'
@ -366,6 +367,7 @@ export const iconMap = {
sync: MsSync,
warning: MaterialSymbolsWarningOutlineRounded,
share: h('span', { class: 'material-symbols' }, 'share'),
mobileShare: MobileShare,
reload: MdiRefresh,
xml: h('span', { class: 'material-symbols' }, 'code'),
airtable: LogosAirtable,
@ -396,6 +398,7 @@ export const iconMap = {
drag: MaterialSymbolsDragIndicator,
comment: h('span', { class: 'material-symbols' }, 'comment'),
doc: h('span', { class: 'material-symbols' }, 'menu_book'),
menu: h('span', { class: 'material-symbols' }, 'menu'),
move: MsMove,
creditCard: NcCreditCard,
heightShort: NcIconsRowHeightShort,

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

@ -55,6 +55,17 @@ export default defineConfig({
mono: ['Inter', 'mono'],
},
extend: {
screens: {
xs: {
max: '480px',
},
sm: {
max: '820px',
},
md: {
min: '820px',
},
},
textColor: {
primary: 'rgba(var(--color-primary), var(--tw-text-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-text-opacity))',

Loading…
Cancel
Save