Browse Source

chore(gui-v2): sync with develop

pull/2838/head
Wing-Kam Wong 2 years ago
parent
commit
a4e6d522da
  1. 5
      packages/nc-gui-v2/assets/style-v2.scss
  2. 1
      packages/nc-gui-v2/components.d.ts
  3. 2
      packages/nc-gui-v2/components/cell/TextArea.vue
  4. 8
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  5. 2
      packages/nc-gui-v2/components/dashboard/settings/AuditTab.vue
  6. 112
      packages/nc-gui-v2/components/dashboard/settings/Metadata.vue
  7. 6
      packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue
  8. 140
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  9. 7
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  10. 2
      packages/nc-gui-v2/components/dlg/QuickImport.vue
  11. 2
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  12. 10
      packages/nc-gui-v2/components/smartsheet-toolbar/AddRow.vue
  13. 73
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  14. 55
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  15. 9
      packages/nc-gui-v2/components/smartsheet-toolbar/DeleteTable.vue
  16. 76
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  17. 673
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  18. 130
      packages/nc-gui-v2/components/smartsheet-toolbar/LockMenu.vue
  19. 197
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  20. 11
      packages/nc-gui-v2/components/smartsheet-toolbar/Reload.vue
  21. 37
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  22. 18
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  23. 115
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  24. 14
      packages/nc-gui-v2/components/smartsheet-toolbar/ToggleDrawer.vue
  25. 252
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  26. 20
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  27. 25
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  28. 43
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  29. 1
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  30. 1
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  31. 79
      packages/nc-gui-v2/composables/useTabs.ts
  32. 5
      packages/nc-gui-v2/composables/useViewColumns.ts
  33. 5
      packages/nc-gui-v2/context/index.ts
  34. 2
      packages/nc-gui-v2/nuxt.config.ts
  35. 43
      packages/nc-gui-v2/package-lock.json
  36. 6
      packages/nc-gui-v2/package.json
  37. 40
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  38. 152
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  39. 11
      packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue
  40. 5
      packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue
  41. 17
      packages/nc-gui-v2/pages/nc/[projectId]/index/index/index.vue
  42. 1
      packages/nc-gui-v2/pages/project/index/create-external.vue
  43. 13
      packages/nocodb-sdk/src/lib/helperFunctions.ts

5
packages/nc-gui-v2/assets/style-v2.scss

@ -64,3 +64,8 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
html { html {
overflow-y: auto !important; overflow-y: auto !important;
} }
.nc-menu-item {
@apply cursor-pointer text-xs flex align-center gap-2 p-4 relative after:(content-[''] absolute top-0 left-0 w-full h-full right 0 bg-current opacity-0 transition transition-opactity duration-100) hover:(after:(opacity-5));
}

1
packages/nc-gui-v2/components.d.ts vendored

@ -7,6 +7,7 @@ export {}
declare module '@vue/runtime-core' { declare module '@vue/runtime-core' {
export interface GlobalComponents { export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAnchorLink: typeof import('ant-design-vue/es')['AnchorLink'] AAnchorLink: typeof import('ant-design-vue/es')['AnchorLink']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete'] AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
AButton: typeof import('ant-design-vue/es')['Button'] AButton: typeof import('ant-design-vue/es')['Button']

2
packages/nc-gui-v2/components/cell/TextArea.vue

@ -2,7 +2,7 @@
import { computed, inject, onMounted, ref } from '#imports' import { computed, inject, onMounted, ref } from '#imports'
interface Props { interface Props {
modelValue: string modelValue?: string
} }
const { modelValue: value } = defineProps<Props>() const { modelValue: value } = defineProps<Props>()

8
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -192,7 +192,7 @@ const reloadTables = async () => {
} }
const addTableTab = (table: TableType) => { const addTableTab = (table: TableType) => {
$e('a:table:open') $e('a:table:open')
addTab({ title: table.title, id: table.id }) addTab({ title: table.title, id: table.id, type: table.type as any })
} }
</script> </script>
@ -208,7 +208,7 @@ const addTableTab = (table: TableType) => {
</div> </div>
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<div class="p-1 flex-1 overflow-y-auto flex flex-column"> <div class="p-1 flex-1 overflow-y-auto flex flex-column scrollbar-thin-primary">
<div <div
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer" class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList" @click="showTableList = !showTableList"
@ -218,7 +218,7 @@ const addTableTab = (table: TableType) => {
<span class="flex-grow text-bold nc-project-tree" <span class="flex-grow text-bold nc-project-tree"
>{{ $t('objects.tables') }} <template v-if="tables?.length">({{ tables.length }})</template></span >{{ $t('objects.tables') }} <template v-if="tables?.length">({{ tables.length }})</template></span
> >
<MdiPlus v-t="['c:table:create:navdraw']" class="text-gray-500" @click.stop="tableCreateDlg = true" /> <MdiPlus v-t="['c:table:create:navdraw']" class="text-gray-500 nc-btn-tbl-add" @click.stop="tableCreateDlg = true" />
<MdiMenuDown <MdiMenuDown
class="transition-transform !duration-100 text-gray-500" class="transition-transform !duration-100 text-gray-500"
:class="{ 'transform rotate-180': showTableList }" :class="{ 'transform rotate-180': showTableList }"
@ -248,7 +248,7 @@ const addTableTab = (table: TableType) => {
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="cursor-pointer"> <a-menu class="cursor-pointer">
<a-menu-item class="!text-xs" @click="showRenameTableDlg(table)"> Rename </a-menu-item> <a-menu-item v-t="" class="!text-xs" @click="showRenameTableDlg(table)"><div>Rename</div></a-menu-item>
<a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item> <a-menu-item class="!text-xs" @click="deleteTable(table)"> Delete</a-menu-item>
</a-menu> </a-menu>
</template> </template>

2
packages/nc-gui-v2/components/dashboard/settings/AuditTab.vue

@ -96,6 +96,6 @@ const columns = [
/> />
</div> </div>
<a-table class="w-full" :data-source="audits ?? []" :columns="columns" :pagination="false" :loading="isLoading" /> <a-table class="w-full" size="small" :data-source="audits ?? []" :columns="columns" :pagination="false" :loading="isLoading" />
</div> </div>
</template> </template>

112
packages/nc-gui-v2/components/dashboard/settings/Metadata.vue

@ -0,0 +1,112 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiDatabaseSync from '~icons/mdi/database-sync'
const { $api } = useNuxtApp()
const { project } = useProject()
const toast = useToast()
let isLoading = $ref(false)
let isDifferent = $ref(false)
let metadiff = $ref<any[]>([])
async function loadMetaDiff() {
try {
if (!project.value?.id) return
isLoading = true
isDifferent = false
metadiff = await $api.project.metaDiffGet(project.value?.id)
for (const model of metadiff) {
if (model.detectedChanges?.length > 0) {
model.syncState = model.detectedChanges.map((el: any) => el?.msg).join(', ')
isDifferent = true
}
}
} catch (e) {
console.error(e)
} finally {
isLoading = false
}
}
async function syncMetaDiff() {
try {
if (!project.value?.id || !isDifferent) return
isLoading = true
await $api.project.metaDiffSync(project.value.id)
toast.info(`Table metadata recreated successfully`)
await loadMetaDiff()
} catch (e: any) {
if (e.response?.status === 402) {
toast.info(e.message)
} else {
toast.error(e.message)
}
} finally {
isLoading = false
}
}
onMounted(async () => {
if (metadiff.length === 0) {
await loadMetaDiff()
}
})
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [
{
title: tableHeaderRenderer('Models'),
dataIndex: 'title',
key: 'title',
},
{
title: tableHeaderRenderer('Sync State'),
dataIndex: 'syncState',
key: 'syncState',
customRender: (value: { text: string }) =>
h('div', { style: { color: value.text ? 'red' : 'gray' } }, value.text || 'No change identified'),
},
]
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-column w-3/5">
<div class="flex flex-row justify-end items-center w-full mb-4">
<a-button class="self-start" @click="loadMetaDiff">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload
</div>
</a-button>
</div>
<a-table
class="w-full"
:data-source="metadiff ?? []"
:columns="columns"
:pagination="false"
:loading="isLoading"
bordered
/>
</div>
<div class="flex place-content-center w-2/5">
<div v-if="isDifferent">
<a-button v-t="['a:proj-meta:meta-data:sync']" type="primary" @click="syncMetaDiff">
<div class="flex items-center gap-2">
<MdiDatabaseSync />
Sync Now
</div>
</a-button>
</div>
<div v-else>
<span><a-alert message="Tables metadata is in sync" type="success" show-icon /></span>
</div>
</div>
</div>
</template>

6
packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue

@ -2,6 +2,8 @@
import type { FunctionalComponent, SVGAttributes } from 'vue' import type { FunctionalComponent, SVGAttributes } from 'vue'
import AuditTab from './AuditTab.vue' import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue' import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import StoreFrontOutline from '~icons/mdi/storefront-outline' import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill' import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple' import MultipleTableIcon from '~icons/mdi/table-multiple'
@ -61,11 +63,11 @@ const tabsInfo: TabGroup = {
subTabs: { subTabs: {
metaData: { metaData: {
title: 'Metadata', title: 'Metadata',
body: () => AuditTab, body: () => Metadata,
}, },
acl: { acl: {
title: 'UI Access Control', title: 'UI Access Control',
body: () => AuditTab, body: () => UIAcl,
}, },
}, },
}, },

140
packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue

@ -0,0 +1,140 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { viewIcons } from '~/utils/viewUtils'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiContentSave from '~icons/mdi/content-save'
import MdiMagnify from '~icons/mdi/magnify'
const { $api, $e } = useNuxtApp()
const { project } = useProject()
const toast = useToast()
const roles = $ref<string[]>(['editor', 'commenter', 'viewer'])
let isLoading = $ref(false)
let tables = $ref<any[]>([])
let searchInput = $ref('')
const filteredTables = computed(() =>
tables.filter(
(el) =>
(typeof el?._ptn === 'string' && el._ptn.toLowerCase().includes(searchInput.toLowerCase())) ||
(typeof el?.title === 'string' && el.title.toLowerCase().includes(searchInput.toLowerCase())),
),
)
async function loadTableList() {
try {
if (!project.value?.id) return
isLoading = true
// TODO includeM2M
tables = await $api.project.modelVisibilityList(project.value?.id, {
includeM2M: '',
})
} catch (e) {
console.error(e)
} finally {
isLoading = false
}
}
async function saveUIAcl() {
try {
if (!project.value?.id) return
await $api.project.modelVisibilitySet(
project.value.id,
tables.filter((t) => t.edited),
)
toast.success('Updated UI ACL for tables successfully')
} catch (e: any) {
toast.error(e?.message)
}
$e('a:proj-meta:ui-acl')
}
const onRoleCheck = (record: any, role: string) => {
record.disabled[role] = !record.disabled[role]
record.edited = true
}
onMounted(async () => {
if (tables.length === 0) {
await loadTableList()
}
})
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [
{
title: tableHeaderRenderer('Table name'),
name: 'table_name',
},
{
title: tableHeaderRenderer('View name'),
name: 'view_name',
},
{
title: tableHeaderRenderer('Editor'),
name: 'editor',
width: 150,
},
{
title: tableHeaderRenderer('Commenter'),
name: 'commenter',
width: 150,
},
{
title: tableHeaderRenderer('Viewer'),
name: 'viewer',
width: 150,
},
]
</script>
<template>
<div class="flex flex-row w-full">
<div class="flex flex-column w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2">
<a-input v-model:value="searchInput" placeholder="Search models">
<template #prefix>
<MdiMagnify />
</template>
</a-input>
<a-button class="self-start" @click="loadTableList">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload
</div>
</a-button>
<a-button class="self-start" @click="saveUIAcl">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiContentSave />
Save
</div>
</a-button>
</div>
<a-table class="w-full" :data-source="filteredTables" :columns="columns" :pagination="false" :loading="isLoading" bordered>
<template #bodyCell="{ record, column }">
<div v-if="column.name === 'table_name'">{{ record._ptn }}</div>
<div v-if="column.name === 'view_name'">
<div class="flex align-center">
<component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" />
{{ record.title }}
</div>
</div>
<div v-for="role in roles" :key="role">
<div v-if="column.name === role">
<a-tooltip>
<template #title>Click to hide '{{ record.title }}' for role:{{ role }} in UI dashboard</template>
<a-checkbox :checked="!record.disabled[role]" @change="onRoleCheck(record, role)"></a-checkbox>
</a-tooltip>
</div>
</div>
</template>
</a-table>
</div>
</div>
</template>

7
packages/nc-gui-v2/components/dlg/AirtableImport.vue

@ -210,7 +210,7 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<a-modal v-model:visible="dialogShow" width="max(90vw, 600px)" @keydown.esc="dialogShow = false"> <a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" @keydown.esc="dialogShow = false">
<template #footer> <template #footer>
<div v-if="step === 1"> <div v-if="step === 1">
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button> <a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
@ -218,6 +218,7 @@ onBeforeUnmount(() => {
key="submit" key="submit"
v-t="['c:sync-airtable:save-and-sync']" v-t="['c:sync-airtable:save-and-sync']"
type="primary" type="primary"
class="nc-btn-airtable-import"
:disabled="disableImportButton" :disabled="disableImportButton"
@click="saveAndSync" @click="saveAndSync"
>Import >Import
@ -239,10 +240,10 @@ onBeforeUnmount(() => {
</div> </div>
<a-form ref="form" :model="syncSource" name="quick-import-airtable-form" layout="horizontal" class="ma-0"> <a-form ref="form" :model="syncSource" name="quick-import-airtable-form" layout="horizontal" class="ma-0">
<a-form-item v-bind="validateInfos['details.apiKey']"> <a-form-item v-bind="validateInfos['details.apiKey']">
<a-input-password v-model:value="syncSource.details.apiKey" placeholder="Api Key" size="large" /> <a-input-password v-model:value="syncSource.details.apiKey" class="nc-input-api-key" placeholder="Api Key" size="large" />
</a-form-item> </a-form-item>
<a-form-item v-bind="validateInfos['details.syncSourceUrlOrId']"> <a-form-item v-bind="validateInfos['details.syncSourceUrlOrId']">
<a-input v-model:value="syncSource.details.syncSourceUrlOrId" placeholder="Shared Base ID / URL" size="large" /> <a-input v-model:value="syncSource.details.syncSourceUrlOrId" class="nc-input-shared-base" placeholder="Shared Base ID / URL" size="large" />
</a-form-item> </a-form-item>
<span class="prose-xl font-bold self-center my-4">Advanced Settings</span> <span class="prose-xl font-bold self-center my-4">Advanced Settings</span>
<a-divider class="mt-2 mb-5" /> <a-divider class="mt-2 mb-5" />

2
packages/nc-gui-v2/components/dlg/QuickImport.vue

@ -228,7 +228,7 @@ function getAdapter(name: string, val: any) {
</script> </script>
<template> <template>
<a-modal v-model:visible="dialogShow" width="max(90vw, 600px)" @keydown.esc="dialogShow = false"> <a-modal v-model:visible="dialogShow" width="max(60vw, 600px)" @keydown.esc="dialogShow = false">
<a-typography-title class="ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</a-typography-title> <a-typography-title class="ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</a-typography-title>
<template #footer> <template #footer>
<a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button> <a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button>

2
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -85,7 +85,7 @@ onMounted(() => {
<template> <template>
<a-modal <a-modal
v-model:visible="dialogShow" v-model:visible="dialogShow"
width="max(90vw, 600px)" width="max(30vw, 600px)"
@keydown.esc="dialogShow = false" @keydown.esc="dialogShow = false"
@keydown.enter="$emit('create', table)" @keydown.enter="$emit('create', table)"
> >

10
packages/nc-gui-v2/components/smartsheet-toolbar/AddRow.vue

@ -0,0 +1,10 @@
<script setup lang="ts">
import MdiAddIcon from '~icons/mdi/plus-outline '
const emit = defineEmits(['add-row'])
</script>
<template>
<MdiAddIcon class="text-grey" @click="emit('add-row')" />
</template>
<style scoped></style>

73
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -7,7 +7,7 @@ import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context' import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewFilters from '~/composables/useViewFilters' import useViewFilters from '~/composables/useViewFilters'
import MdiDeleteIcon from '~icons/mdi/close-box' import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
const { nested = false, parentId } = defineProps<{ nested?: boolean; parentId?: string }>() const { nested = false, parentId } = defineProps<{ nested?: boolean; parentId?: string }>()
const meta = inject(MetaInj) const meta = inject(MetaInj)
@ -72,13 +72,13 @@ watch(
</script> </script>
<template> <template>
<div class="backgroundColor pa-2 menu-filter-dropdown bg-background" :style="{ width: nested ? '100%' : '630px' }"> <div class="bg-white shadow pa-2 menu-filter-dropdown" :style="{ width: nested ? '100%' : '630px' }">
<div v-if="filters && filters.length" class="grid" @click.stop> <div v-if="filters && filters.length" class="grid" @click.stop>
<template v-for="(filter, i) in filters" :key="filter.id || i"> <template v-for="(filter, i) in filters" :key="filter.id || i">
<template v-if="filter.status !== 'delete'"> <template v-if="filter.status !== 'delete'">
<div v-if="filter.is_group" :key="i" style="grid-column: span 4; padding: 6px" class="elevation-4"> <div v-if="filter.is_group" :key="i" style="grid-column: span 4; padding: 6px" class="elevation-4">
<div class="d-flex" style="gap: 6px; padding: 0 6px"> <div class="d-flex" style="gap: 6px; padding: 0 6px">
<v-icon <!-- <v-icon
v-if="!filter.readOnly" v-if="!filter.readOnly"
:key="`${i}_3`" :key="`${i}_3`"
small small
@ -86,10 +86,18 @@ watch(
@click.stop="deleteFilter(filter, i)" @click.stop="deleteFilter(filter, i)"
> >
mdi-close-box mdi-close-box
</v-icon> </v-icon> -->
<MdiDeleteIcon
v-if="!filter.readOnly"
small
class="nc-filter-item-remove-btn"
@click.stop="deleteFilter(filter, i)"
/>
<span v-else :key="`${i}_1`" /> <span v-else :key="`${i}_1`" />
<v-select
v-model="filter.logical_op" <a-select
v-model:value="filter.logical_op"
class="flex-shrink-1 flex-grow-0 elevation-0 caption" class="flex-shrink-1 flex-grow-0 elevation-0 caption"
:items="['and', 'or']" :items="['and', 'or']"
density="compact" density="compact"
@ -102,7 +110,7 @@ watch(
<!-- <template #item="{ item }"> --> <!-- <template #item="{ item }"> -->
<!-- <span class="caption font-weight-regular">{{ item }}</span> --> <!-- <span class="caption font-weight-regular">{{ item }}</span> -->
<!-- </template> --> <!-- </template> -->
</v-select> </a-select>
</div> </div>
<!-- <column-filter <!-- <column-filter
v-if="filter.id || shared" v-if="filter.id || shared"
@ -134,28 +142,24 @@ watch(
v-if="!filter.readOnly" v-if="!filter.readOnly"
class="nc-filter-item-remove-btn text-grey align-self-center" class="nc-filter-item-remove-btn text-grey align-self-center"
@click.stop="deleteFilter(filter, i)" @click.stop="deleteFilter(filter, i)"
></MdiDeleteIcon> />
<span v-else /> <span v-else />
<span v-if="!i" :key="`${i}_2`" class="text-xs d-flex align-center">{{ $t('labels.where') }}</span> <span v-if="!i" class="text-xs d-flex align-center">{{ $t('labels.where') }}</span>
<v-select <a-select
v-else v-else
:key="`${i}_4`" v-model:value="filter.logical_op"
v-model="filter.logical_op" class="h-full"
class="w-full elevation-0 caption" :options="[
:items="['and', 'or']" { value: 'and', text: 'AND' },
density="compact" { value: 'or', text: 'OR' },
variant="solo" ]"
hide-details hide-details
:disabled="filter.readOnly" :disabled="filter.readOnly"
@click.stop @click.stop
@change="filterUpdateCondition(filter, i)" @change="filterUpdateCondition(filter, i)"
/> />
<!-- <template #item="{ item }">
<span class="caption font-weight-regular">{{ item }}</span>
</template>
</v-select> -->
<FieldListAutoCompleteDropdown <FieldListAutoCompleteDropdown
:key="`${i}_6`" :key="`${i}_6`"
@ -167,10 +171,10 @@ watch(
@change="saveOrUpdate(filter, i)" @change="saveOrUpdate(filter, i)"
/> />
<v-select <a-select
v-model="filter.comparison_op" v-model:value="filter.comparison_op"
class="caption nc-filter-operation-select text-sm" class="caption nc-filter-operation-select text-sm"
:items="comparisonOpList.map((it) => it.value)" :options="comparisonOpList"
:placeholder="$t('labels.operation')" :placeholder="$t('labels.operation')"
density="compact" density="compact"
variant="solo" variant="solo"
@ -189,21 +193,17 @@ watch(
<!-- </template> --> <!-- </template> -->
<!-- </v-select> --> <!-- </v-select> -->
<span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="`span${i}`" /> <span v-if="['null', 'notnull', 'empty', 'notempty'].includes(filter.comparison_op)" :key="`span${i}`" />
<v-checkbox <a-checkbox
v-else-if="types[filter.field] === 'boolean'" v-else-if="types[filter.field] === 'boolean'"
:key="`${i}_7`" v-model:value="filter.value"
v-model="filter.value"
dense dense
:disabled="filter.readOnly" :disabled="filter.readOnly"
@change="saveOrUpdate(filter, i)" @change="saveOrUpdate(filter, i)"
/> />
<v-text-field <a-input
v-else v-else
:key="`${i}_7`" :key="`${i}_7`"
v-model="filter.value" v-model="filter.value"
density="compact"
variant="solo"
hide-details
class="caption text-sm nc-filter-value-select" class="caption text-sm nc-filter-value-select"
:disabled="filter.readOnly" :disabled="filter.readOnly"
@click.stop @click.stop
@ -214,11 +214,14 @@ watch(
</template> </template>
</div> </div>
<v-btn small class="elevation-0 text-sm text-capitalize text-grey my-3" @click.stop="addFilter"> <a-button small class="elevation-0 text-sm text-capitalize text-grey my-3" @click.stop="addFilter">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> --> <div class="flex align-center gap-1">
<!-- Add Filter --> <!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
{{ $t('activity.addFilter') }} <MdiAddIcon />
</v-btn> <!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</a-button>
<slot /> <slot />
</div> </div>
</template> </template>

55
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue

@ -16,50 +16,19 @@ const applyChanges = () => {}
</script> </script>
<template> <template>
<v-menu offset-y eager transition="slide-y-transition"> <a-dropdown :trigger="['click']">
<template #activator="{ props }"> <v-badge :value="filters.length" color="primary" dot overlap>
<v-badge :value="filters.length" color="primary" dot overlap> <a-button v-t="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
<v-btn <div class="flex align-center gap-1">
v-t="['c:filter']" <MdiFilterIcon class="text-grey" />
class="nc-filter-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small
text
:class="{
'primary lighten-5 grey--text text--darken-3': filters.length,
}"
v-bind="props"
>
<MdiFilterIcon class="mr-1 text-grey" />
<!-- Filter --> <!-- Filter -->
<span class="text-capitalize">{{ $t('activity.filter') }}</span> <span class="text-capitalize nc-filter-menu-btn">{{ $t('activity.filter') }}</span>
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDownIcon class="text-grey" />
</v-btn> </div>
</v-badge> </a-button>
</v-badge>
<template #overlay>
<SmartsheetToolbarColumnFilter />
</template> </template>
<SmartsheetToolbarColumnFilter> </a-dropdown>
<!-- <div class="d-flex align-center mx-2" @click.stop>
<v-checkbox
id="col-filter-checkbox"
v-model="autoApplyFilter"
class="col-filter-checkbox"
hide-details
dense
type="checkbox"
color="grey"
>
<template #label>
<span class="grey&#45;&#45;text caption">
{{ $t('msg.info.filterAutoApply') }}
&lt;!&ndash; Auto apply &ndash;&gt;
</span>
</template>
</v-checkbox>
<v-spacer />
<v-btn v-show="!autoApplyFilter" color="primary" small class="caption ml-2" @click="applyChanges"> Apply changes </v-btn>
</div> -->
</SmartsheetToolbarColumnFilter>
</v-menu>
</template> </template>

9
packages/nc-gui-v2/components/smartsheet-toolbar/DeleteTable.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
import MdiDeleteIcon from '~icons/mdi/delete-outline'
</script>
<template>
<MdiDeleteIcon class="text-grey" />
</template>
<style scoped></style>

76
packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue

@ -1,4 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SelectProps } from 'ant-design-vue'
import { computed } from 'vue'
import { MetaInj } from '~/context' import { MetaInj } from '~/context'
interface Props { interface Props {
@ -42,37 +44,55 @@ const localValue = computed({
}, },
}, },
} */ } */
const options = computed<SelectProps['options']>(() =>
meta?.value?.columns?.map((c) => ({
value: c.id,
label: c.title,
})),
)
const filterOption = (input: string, option: any) => {
return option.value.toLowerCase()?.includes(input.toLowerCase())
}
</script> </script>
<template> <template>
<v-autocomplete <a-select
ref="field" v-model:value="localValue"
v-model="localValue" show-search
class="caption" placeholder="Select a field"
:items="meta.columns" :options="options"
item-value="id" :filter-option="filterOption"
item-text="title" ></a-select>
:label="$t('objects.field')"
variant="solo" <!-- <v-autocomplete
hide-details ref="field"
@click.stop v-model="localValue"
> class="caption"
<!-- &lt;!&ndash; @change="$emit('change')" &ndash;&gt; --> :items="meta.columns"
<!-- <template #selection="{ item }"> --> item-value="id"
<!-- <v-icon small class="mr-1"> --> item-text="title"
<!-- {{ item.icon }} --> :label="$t('objects.field')"
<!-- </v-icon> --> variant="solo"
<!-- {{ item.title }} --> hide-details
<!-- </template> --> @click.stop
<!-- <template #item="{ item }"> --> >
<!-- <span :class="`caption font-weight-regular nc-fld-${item.title}`"> --> &lt;!&ndash; &lt;!&ndash; @change="$emit('change')" &ndash;&gt; &ndash;&gt;
<!-- <v-icon color="grey" small class="mr-1"> --> &lt;!&ndash; <template #selection="{ item }"> &ndash;&gt;
<!-- {{ item.icon }} --> &lt;!&ndash; <v-icon small class="mr-1"> &ndash;&gt;
<!-- </v-icon> --> &lt;!&ndash; {{ item.icon }} &ndash;&gt;
<!-- {{ item.title }} --> &lt;!&ndash; </v-icon> &ndash;&gt;
<!-- </span> --> &lt;!&ndash; {{ item.title }} &ndash;&gt;
<!-- </template> --> &lt;!&ndash; </template> &ndash;&gt;
</v-autocomplete> &lt;!&ndash; <template #item="{ item }"> &ndash;&gt;
&lt;!&ndash; <span :class="`caption font-weight-regular nc-fld-${item.title}`"> &ndash;&gt;
&lt;!&ndash; <v-icon color="grey" small class="mr-1"> &ndash;&gt;
&lt;!&ndash; {{ item.icon }} &ndash;&gt;
&lt;!&ndash; </v-icon> &ndash;&gt;
&lt;!&ndash; {{ item.title }} &ndash;&gt;
&lt;!&ndash; </span> &ndash;&gt;
&lt;!&ndash; </template> &ndash;&gt;
</v-autocomplete> -->
</template> </template>
<style scoped></style> <style scoped></style>

673
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -1,9 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context' import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import useViewColumns from '~/composables/useViewColumns' import useViewColumns from '~/composables/useViewColumns'
import MdiMenuDownIcon from '~icons/mdi/menu-down' import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiEyeIcon from '~icons/mdi/eye-off-outline' import MdiEyeIcon from '~icons/mdi/eye-off-outline'
import MdiDragIcon from '~icons/mdi/drag'
const { fieldsOrder, coverImageField, modelValue } = defineProps<{ const { fieldsOrder, coverImageField, modelValue } = defineProps<{
coverImageField?: string coverImageField?: string
@ -23,6 +25,8 @@ const isAnyFieldHidden = computed(() => {
// return meta?.fields?.some(field => field.hidden) // return meta?.fields?.some(field => field.hidden)
}) })
const { $e } = useNuxtApp()
const { const {
showSystemFields, showSystemFields,
sortedAndFilteredFields, sortedAndFilteredFields,
@ -33,6 +37,7 @@ const {
showAll, showAll,
hideAll, hideAll,
saveOrUpdate, saveOrUpdate,
sortedFields,
} = useViewColumns(activeView, meta, false, () => reloadDataHook?.trigger()) } = useViewColumns(activeView, meta, false, () => reloadDataHook?.trigger())
watch( watch(
@ -52,634 +57,76 @@ watch(
{ immediate: true }, { immediate: true },
) )
/* import draggable from 'vuedraggable' const onMove = (event) => {
import { getSystemColumnsIds } from 'nocodb-sdk' // todo : sync with server
import { getUIDTIcon } from '~/components/project/spreadsheet/helpers/uiTypes' // if (!sortedFields?.value) return
// if (sortedFields?.value.length - 1 === event.moved.newIndex) {
export default { // sortedFields.value[event.moved.newIndex].order = sortedFields.value[event.moved.newIndex - 1].order + 1
name: 'FieldsMenu', // } else if (event.moved.newIndex === 0) {
components: { // sortedFields.value[event.moved.newIndex].order = sortedFields.value[1].order / 2
Draggable: draggable, // } else {
}, // sortedFields.value[event.moved.newIndex].order =
props: { // (sortedFields?.value[event.moved.newIndex - 1].order + sortedFields?.value[event.moved.newIndex + 1].order) / 2
coverImageField: String, // // );
groupingField: String, // }
isGallery: Boolean, // saveOrUpdate(sortedFields[event.moved.newIndex], event.moved.newIndex);
isKanban: Boolean, $e('a:fields:reorder')
sqlUi: [Object, Function], }
meta: Object,
fieldsOrder: [Array],
value: [Object, Array],
fieldList: [Array, Object],
showSystemFields: {
type: [Boolean, Number],
default: false,
},
isLocked: Boolean,
isPublic: Boolean,
viewId: String,
},
data: () => ({
fields: [],
fieldFilter: '',
showFields: {},
fieldsOrderLoc: [],
}),
computed: {
systemColumnsIds() {
return getSystemColumnsIds(this.meta && this.meta.columns)
},
attachmentFields() {
return [
...(this.meta && this.meta.columns ? this.meta.columns.filter((f) => f.uidt === 'Attachment') : []),
{
alias: 'None',
id: null,
},
]
},
singleSelectFields() {
return [
...(this.meta && this.meta.columns ? this.meta.columns.filter((f) => f.uidt === 'SingleSelect') : []),
{
alias: 'None',
id: null,
},
]
},
coverImageFieldLoc: {
get() {
return this.coverImageField
},
set(val) {
this.$emit('update:coverImageField', val)
},
},
groupingFieldLoc: {
get() {
return this.groupingField
},
set(val) {
this.$emit('update:groupingField', val)
},
},
columnMeta() {
return this.meta && this.meta.columns
? this.meta.columns.reduce(
(o, c) => ({
...o,
[c.title]: c,
}),
{},
)
: {}
},
isAnyFieldHidden() {
return this.fields.some((f) => !(!this.showSystemFieldsLoc && this.systemColumnsIds.includes(f.fk_column_id)) && !f.show) // Object.values(this.showFields).some(v => !v)
},
showSystemFieldsLoc: {
get() {
return this.showSystemFields
},
set(v) {
this.$emit('update:showSystemFields', v)
this.showFields = this.fields.reduce((o, c) => ({ [c.title]: c.show, ...o }), {})
this.$emit(
'update:fieldsOrder',
this.fields.map((c) => c.title),
)
this.$e('a:fields:system-fields')
},
},
},
watch: {
async viewId(v) {
if (v) {
await this.loadFields()
}
},
fieldList(f) {
this.fieldsOrderLoc = [...f]
},
showFields: {
handler(v) {
this.$nextTick(() => {
this.$emit('input', v)
})
},
deep: true,
},
value(v) {
this.showFields = v || []
},
fieldsOrder(n, o) {
if ((n && n.join()) !== (o && o.join())) {
this.fieldsOrderLoc = n
}
this.fieldsOrderLoc = n && n.length ? n : [...this.fieldList]
},
fieldsOrderLoc: {
handler(n, o) {
if ((n && n.join()) !== (o && o.join())) {
this.$emit('update:fieldsOrder', n)
}
},
deep: true,
},
},
created() {
this.loadFields()
this.showFields = this.value
this.fieldsOrderLoc = this.fieldsOrder && this.fieldsOrder.length ? this.fieldsOrder : [...this.fieldList]
},
methods: {
async loadFields() {
let fields = []
let order = 1
if (this.viewId) {
const data = await this.$api.dbViewColumn.list(this.viewId)
const fieldById = data.reduce(
(o, f) => ({
...o,
[f.fk_column_id]: f,
}),
{},
)
fields = this.meta.columns
.map((c) => ({
title: c.title,
fk_column_id: c.id,
...(fieldById[c.id] ? fieldById[c.id] : {}),
order: (fieldById[c.id] && fieldById[c.id].order) || order++,
icon: getUIDTIcon(c.uidt),
}))
.sort((a, b) => a.order - b.order)
} else if (this.isPublic) {
fields = this.meta.columns
}
this.fields = fields
this.$emit(
'input',
this.fields.reduce(
(o, c) => ({
...o,
[c.title]: c.show,
}),
{},
),
)
this.$emit(
'update:fieldsOrder',
this.fields.map((c) => c.title),
)
},
async saveOrUpdate(field, i) {
if (!this.isPublic && this._isUIAllowed('fieldsSync')) {
if (field.id) {
await this.$api.dbViewColumn.update(this.viewId, field.id, field)
} else {
this.fields[i] = await this.$api.dbViewColumn.create(this.viewId, field)
}
}
this.$emit('updated')
this.$emit(
'input',
this.fields.reduce(
(o, c) => ({
...o,
[c.title]: c.show,
}),
{},
),
)
this.$emit(
'update:fieldsOrder',
this.fields.map((c) => c.title),
)
this.$e('a:fields:show-hide')
},
async showAll() {
if (!this.isPublic) {
await this.$api.dbView.showAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = true
}
this.$emit('updated')
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => ((o[k] = true), o), {})
this.$e('a:fields:show-all')
},
async hideAll() {
if (!this.isPublic) {
await this.$api.dbView.hideAllColumn(this.viewId)
}
for (const f of this.fields) {
f.show = false
}
this.$emit('updated')
this.$nextTick(() => {
this.showFields = (this.fieldsOrderLoc || Object.keys(this.showFields)).reduce((o, k) => ((o[k] = false), o), {})
})
this.$e('a:fields:hide-all')
},
onMove(event) {
if (this.fields.length - 1 === event.moved.newIndex) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[event.moved.newIndex - 1].order + 1)
} else if (event.moved.newIndex === 0) {
this.$set(this.fields[event.moved.newIndex], 'order', this.fields[1].order / 2)
} else {
this.$set(
this.fields[event.moved.newIndex],
'order',
(this.fields[event.moved.newIndex - 1].order + this.fields[event.moved.newIndex + 1].order) / 2,
)
}
this.saveOrUpdate(this.fields[event.moved.newIndex], event.moved.newIndex)
this.$e('a:fields:reorder')
},
},
} */
</script> </script>
<template> <template>
<v-menu> <a-dropdown :trigger="['click']">
<template #activator="{ props }"> <v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap v-bind="props"> <a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked" size="small">
<v-btn <div class="flex align-center gap-1">
v-t="['c:fields']"
class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small
text
:class="{
'primary lighten-5 grey--text text--darken-3': isAnyFieldHidden,
}"
>
<!-- <v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon> --> <!-- <v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon> -->
<MdiEyeIcon class="mr-1 text-grey"></MdiEyeIcon> <MdiEyeIcon class="text-grey"></MdiEyeIcon>
<!-- Fields --> <!-- Fields -->
<span class="text-sm text-capitalize">{{ $t('objects.fields') }}</span> <span class="text-sm text-capitalize nc-fields-menu-btn">{{ $t('objects.fields') }}</span>
<MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon> <MdiMenuDownIcon class="text-grey"></MdiMenuDownIcon>
</v-btn>
</v-badge>
</template>
<v-list density="compact" class="pt-0" min-width="280" @click.stop>
<div class="nc-fields-list py-1">
<!-- <Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)"> -->
<v-list-item v-for="(field, i) in filteredFieldList" :key="field.id" dense>
<input
:id="`show-field-${field.id}`"
v-model="field.show"
type="checkbox"
class="mt-0 pt-0"
@click.stop
@change="saveOrUpdate(field, i)"
/>
<!-- @change="saveOrUpdate(field, i)"> -->
<!-- <template #label>
&lt;!&ndash; <v-icon small class="mr-1">
{{ field.icon }}
</v-icon> &ndash;&gt;
<span class="caption">{{ field.title }}</span>
</template> -->
<!-- </input> -->
<label :for="`show-field-${field.id}`" class="ml-2 text-sm">{{ field.title }}</label>
<v-spacer />
<!-- <v-icon small color="grey" :class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"> mdi-drag </v-icon> -->
</v-list-item>
<!-- </Draggable> -->
</div>
<v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense>
<!--
show_system_fields
<v-checkbox v-model="showSystemFields" class="mt-0 pt-0" dense hide-details @click.stop>
<template #label>
<span class="caption text-sm">
&lt;!&ndash; Show System Fields &ndash;&gt;
{{ $t('activity.showSystemFields') }}
</span>
</template>
</v-checkbox> -->
<input :id="`${activeView?.id}-show-system-fields`" v-model="showSystemFields" type="checkbox" />
<label :for="`${activeView.id}-show-system-fields`" class="caption text-sm ml-2">{{
$t('activity.showSystemFields')
}}</label>
</v-list-item>
<v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey--text text-sm text-capitalize" @click.stop="showAll">
<!-- Show All -->
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey--text text-sm text-capitalize" @click.stop="hideAll">
<!-- Hide All -->
{{ $t('general.hideAll') }}
</v-btn>
</v-list-item>
</v-list>
<!--
<v-list dense class="pt-0" min-width="280" @click.stop>
<template v-if="isGallery && _isUIAllowed('updateCoverImage')">
<div class="pa-2">
<v-select
v-model="coverImageFieldLoc"
label="Cover Image"
class="caption field-caption"
dense
outlined
:items="attachmentFields"
item-text="alias"
item-value="id"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-image </v-icon>
</template>
</v-select>
</div> </div>
<v-divider /> </a-button>
</template> </v-badge>
<template v-if="isKanban"> <template #overlay>
<div class="pa-2"> <div class="pt-0 min-w-[280px] bg-white shadow" @click.stop>
<v-select <div class="nc-fields-list py-1">
v-model="groupingFieldLoc" <Draggable :list="sortedFields" @change="onMove($event)">
label="Grouping Field" <template #item="{ element: field }">
class="caption field-caption" <div :key="field.id" class="px-2 py-1 flex" @click.stop>
dense <a-checkbox v-model:checked="field.show" class="flex-shrink" @change="saveOrUpdate(field, i)">
outlined <span class="text-xs">{{ field.title }}</span>
:items="singleSelectFields" </a-checkbox>
item-text="alias" <div class="flex-1" />
item-value="title" <MdiDragIcon class="cursor-move" />
hide-details </div>
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-select-group </v-icon>
</template> </template>
</v-select> </Draggable>
</div> </div>
<v-divider /> <v-divider class="my-2" />
</template>
<v-list-item dense class="">
<v-text-field
v-model="fieldFilter"
dense
flat
class="caption mt-3 mb-2"
color="grey"
:placeholder="$t('placeholder.searchFields')"
hide-details
@click.stop
>
&lt;!&ndash; <template v-slot:prepend-inner>
<v-icon small color="grey" class="mt-2">
mdi-magnify
</v-icon>
</template> &ndash;&gt;
</v-text-field>
</v-list-item>
<div class="nc-fields-list py-1">
&lt;!&ndash; <Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)"> &ndash;&gt;
<template v-for="(field, i) in fields">
<v-list-item
v-show="
(!fieldFilter || (field.title || '').toLowerCase().includes(fieldFilter.toLowerCase())) &&
!(!showSystemFieldsLoc && systemColumnsIds.includes(field.fk_column_id))
"
:key="field.id"
dense
>
<v-checkbox v-model="field.show" class="mt-0 pt-0" dense hide-details @click.stop @change="saveOrUpdate(field, i)">
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon small color="grey" :class="`align-self-center drag-icon nc-child-draggable-icon-${field}`"> mdi-drag </v-icon>
</v-list-item>
</template>
&lt;!&ndash; </Draggable> &ndash;&gt;
</div>
<v-divider class="my-2" />
<v-list-item v-if="!isPublic" dense>
<v-checkbox v-model="showSystemFieldsLoc" class="mt-0 pt-0" dense hide-details @click.stop>
<template #label>
<span class="caption">
&lt;!&ndash; Show System Fields &ndash;&gt;
{{ $t('activity.showSystemFields') }}
</span>
</template>
</v-checkbox>
</v-list-item>
<v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="showAll">
&lt;!&ndash; Show All &ndash;&gt;
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="hideAll">
&lt;!&ndash; Hide All &ndash;&gt;
{{ $t('general.hideAll') }}
</v-btn>
</v-list-item>
</v-list> -->
</v-menu>
<!-- <v-menu offset-y transition="slide-y-transition">
<template #activator="{ on }">
<v-badge :value="isAnyFieldHidden" color="primary" dot overlap>
<v-btn
v-t="['c:fields']"
class="nc-fields-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
outlined
small
text
:class="{
'primary lighten-5 grey&#45;&#45;text text&#45;&#45;darken-3': isAnyFieldHidden,
}"
v-on="on"
>
<v-icon small class="mr-1" color="#777"> mdi-eye-off-outline </v-icon>
&lt;!&ndash; Fields &ndash;&gt;
{{ $t('objects.fields') }}
<v-icon small color="#777"> mdi-menu-down </v-icon>
</v-btn>
</v-badge>
</template>
<v-list dense class="pt-0" min-width="280" @click.stop> <div class="p-2 py-1 flex" @click.stop>
<template v-if="isGallery && _isUIAllowed('updateCoverImage')"> <a-checkbox v-model:checked="showSystemFields">
<div class="pa-2"> <span class="text-xs"> {{ $t('activity.showSystemFields') }}</span>
<v-select </a-checkbox>
v-model="coverImageFieldLoc"
label="Cover Image"
class="caption field-caption"
dense
outlined
:items="attachmentFields"
item-text="alias"
item-value="id"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-image </v-icon>
</template>
</v-select>
</div> </div>
<v-divider /> <div class="p-2 flex gap-2" @click.stop>
</template> <a-button size="small" class="text-gray-500 text-sm text-capitalize" @click.stop="showAll">
<!-- Show All -->
<template v-if="isKanban"> {{ $t('general.showAll') }}
<div class="pa-2"> </a-button>
<v-select <a-button size="small" class="text-gray-500 text-sm text-capitalize" @click.stop="hideAll">
v-model="groupingFieldLoc" <!-- Hide All -->
label="Grouping Field" {{ $t('general.hideAll') }}
class="caption field-caption" </a-button>
dense
outlined
:items="singleSelectFields"
item-text="alias"
item-value="title"
hide-details
@click.stop
>
<template #prepend-inner>
<v-icon small class="field-icon"> mdi-select-group </v-icon>
</template>
</v-select>
</div> </div>
<v-divider />
</template>
<v-list-item dense class="">
<v-text-field
v-model="fieldFilter"
dense
flat
class="caption mt-3 mb-2"
color="grey"
:placeholder="$t('placeholder.searchFields')"
hide-details
@click.stop
>
&lt;!&ndash; <template v-slot:prepend-inner>
<v-icon small color="grey" class="mt-2">
mdi-magnify
</v-icon>
</template> &ndash;&gt;
</v-text-field>
</v-list-item>
<div class="nc-fields-list py-1">
<Draggable v-model="fields" @start="drag = true" @end="drag = false" @change="onMove($event)">
<template v-for="(field, i) in fields">
<v-list-item
v-show="
(!fieldFilter || (field.title || '').toLowerCase().includes(fieldFilter.toLowerCase())) &&
!(!showSystemFieldsLoc && systemColumnsIds.includes(field.fk_column_id))
"
:key="field.id"
dense
>
<v-checkbox v-model="field.show" class="mt-0 pt-0" dense hide-details @click.stop @change="saveOrUpdate(field, i)">
<template #label>
<v-icon small class="mr-1">
{{ field.icon }}
</v-icon>
<span class="caption">{{ field.title }}</span>
</template>
</v-checkbox>
<v-spacer />
<v-icon small color="grey" :class="`align-self-center drag-icon nc-child-draggable-icon-${field}`">
mdi-drag
</v-icon>
</v-list-item>
</template>
</Draggable>
</div> </div>
<v-divider class="my-2" /> </template>
</a-dropdown>
<v-list-item v-if="!isPublic" dense>
<v-checkbox v-model="showSystemFieldsLoc" class="mt-0 pt-0" dense hide-details @click.stop>
<template #label>
<span class="caption">
&lt;!&ndash; Show System Fields &ndash;&gt;
{{ $t('activity.showSystemFields') }}
</span>
</template>
</v-checkbox>
</v-list-item>
<v-list-item dense class="mt-2 list-btn mb-3">
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="showAll">
&lt;!&ndash; Show All &ndash;&gt;
{{ $t('general.showAll') }}
</v-btn>
<v-btn small class="elevation-0 grey&#45;&#45;text" @click.stop="hideAll">
&lt;!&ndash; Hide All &ndash;&gt;
{{ $t('general.hideAll') }}
</v-btn>
</v-list-item>
</v-list>
</v-menu> -->
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
/*::v-deep { :deep(.ant-checkbox-input) {
.v-list-item { transform: scale(0.7);
min-height: 30px;
}
.v-input--checkbox .v-icon {
font-size: 12px !important;
}
.field-caption {
.v-input__append-inner {
margin-top: 4px !important;
}
.v-input__slot {
min-height: 25px !important;
}
&.v-input input {
max-height: 20px !important;
}
.field-icon {
margin-top: 2px;
}
}
} }
.drag-icon {
cursor: all-scroll; !*cursor: grab;*!
}
.nc-fields-list {
height: auto;
max-height: 500px;
overflow-y: auto;
}*/
</style> </style>

130
packages/nc-gui-v2/components/smartsheet-toolbar/LockMenu.vue

@ -1,5 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from '@vue/reactivity'
import { useToast } from 'vue-toastification' import { useToast } from 'vue-toastification'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
import MdiCheckIcon from '~icons/mdi/check-bold'
interface Props { interface Props {
modelValue?: LockType modelValue?: LockType
@ -32,6 +37,18 @@ function changeLockType(type: LockType) {
toast.success(`Successfully Switched to ${type} view`, { timeout: 3000 }) toast.success(`Successfully Switched to ${type} view`, { timeout: 3000 })
} }
const Icon = computed(() => {
switch (vModel.value) {
case LockType.Personal:
return MdiAccountIcon
case LockType.Locked:
return MdiLockOutlineIcon
case LockType.Collaborative:
default:
return MdiAccountGroupIcon
}
})
</script> </script>
<script lang="ts"> <script lang="ts">
@ -41,68 +58,57 @@ export default {
</script> </script>
<template> <template>
<v-menu offset-y max-width="350"> <a-dropdown max-width="350" :trigger="['click']">
<template #activator="{ props: menuProps }"> <Icon class="mx-1 nc-view-lock-menu text-grey"> mdi-lock-outline </Icon>
<v-icon v-if="vModel === LockType.Locked" small class="mx-1 nc-view-lock-menu" v-bind="menuProps.onClick"> <template #overlay>
mdi-lock-outline <div class="min-w-[350px] max-w-[500px] shadow bg-white">
</v-icon> <div>
<v-icon v-else-if="vModel === LockType.Personal" small class="mx-1 nc-view-lock-menu" v-bind="menuProps.onClick"> <div class="nc-menu-item">
mdi-account <MdiCheckIcon v-if="!vModel || vModel === LockType.Collaborative" />
</v-icon> <span v-else />
<v-icon v-else small class="mx-1 nc-view-lock-menu" v-bind="menuProps.onClick"> mdi-account-group-outline </v-icon>
<div>
<MdiAccountGroupIcon />
Collaborative view
<div class="nc-subtitle">Collaborators with edit permissions or higher can change the view configuration.</div>
</div>
</div>
<div class="nc-menu-item">
<MdiCheckIcon v-if="vModel === LockType.Locked" />
<span v-else />
<div>
<MdiLockOutlineIcon />
Locked View
<div class="nc-subtitle">No one can edit the view configuration until it is unlocked.</div>
</div>
</div>
<div class="nc-menu-item">
<MdiCheckIcon v-if="vModel === LockType.Personal" />
<span v-else />
<div>
<MdiAccountIcon />
Personal view
<div class="nc-subtitle">
Only you can edit the view configuration. Other collaborators personal views are hidden by default.
</div>
</div>
</div>
</div>
</div>
</template> </template>
<v-list maxc-width="350"> </a-dropdown>
<v-list-item two-line class="pb-4" @click="changeLockType(LockType.Collaborative)">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="!vModel || vModel === LockType.Collaborative" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content class="pb-1">
<v-list-item-title>
<v-icon small class="mt-n1" color="primary"> mdi-account-group </v-icon>
Collaborative view
</v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal">
Collaborators with edit permissions or higher can change the view configuration.
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item two-line class="pb-4" @click="changeLockType(LockType.Locked)">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="vModel === LockType.Locked" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content class="pb-1">
<v-list-item-title>
<v-icon small class="mt-n1" color="primary"> mdi-lock </v-icon>
Locked View
</v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal">
No one can edit the view configuration until it is unlocked.
</v-list-item-subtitle>
<span class="caption mt-3"><v-icon class="mr-1 mt-n1" x-small color="#fcb401"> mdi-star</v-icon>Locked view.</span>
</v-list-item-content>
</v-list-item>
<v-list-item three-line @click="changeLockType(LockType.Personal)">
<v-list-item-icon class="mr-1 align-self-center">
<v-icon v-if="vModel === LockType.Personal" small> mdi-check-bold </v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
<v-icon small class="mt-n1" color="primary"> mdi-account </v-icon>
Personal view
</v-list-item-title>
<v-list-item-subtitle class="pt-2 pl- font-weight-light" style="white-space: normal">
Only you can edit the view configuration. Other collaborators personal views are hidden by default.
</v-list-item-subtitle>
<span class="caption mt-3"><v-icon class="mr-1 mt-n1" x-small color="#fcb401"> mdi-star</v-icon>Coming soon.</span>
</v-list-item-content>
</v-list-item>
</v-list>
</v-menu>
</template> </template>
<style scoped></style> <style scoped>
.nc-menu-item {
@apply grid grid-cols-[30px,auto] gap-2 p-4;
}
.nc-menu-option > :first-child {
@apply align-self-center;
}
.nc-subtitle {
@apply font-size-sm font-weight-light;
}
</style>

197
packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue

@ -1,93 +1,126 @@
<script setup lang="ts"> <script lang="ts" setup>
import MdiFlashOutlineIcon from '~icons/mdi/flash-outline' import { ExportTypes } from 'nocodb-sdk'
import MdiDownloadOutlineIcon from '~icons/mdi/download-outline' import { useToast } from 'vue-toastification'
import MdiUploadOutlineIcon from '~icons/mdi/upload-outline' import FileSaver from 'file-saver'
import MdiViewListOutlineIcon from '~icons/mdi/view-list-outline' import { useNuxtApp } from '#app'
import useProject from '~/composables/useProject'
import { ActiveViewInj, MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiFlashIcon from '~icons/mdi/flash-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiDownloadIcon from '~icons/mdi/download-outline'
import MdiUploadIcon from '~icons/mdi/upload-outline'
import MdiHookIcon from '~icons/mdi/hook' import MdiHookIcon from '~icons/mdi/hook'
import MdiViewListIcon from '~icons/mdi/view-list-outline'
const { isUIAllowed } = useUIPermission() // todo : replace with inject
// TODO:: identify based on meta const publicViewId = null
const isView = ref(false) const { project } = useProject()
const showWebhookDrawer = ref(false)
function exportCsv() { const { $api } = useNuxtApp()
// TODO const toast = useToast()
}
function importCsv() { const meta = inject(MetaInj)
// TODO const selectedView = inject(ActiveViewInj)
}
function openSharedViewModal() { const exportCsv = async () => {
// TODO: let offset = 0
} let c = 1
try {
while (!isNaN(offset) && offset > -1) {
let res
if (publicViewId) {
/* res = await this.$api.public.csvExport(this.publicViewId, ExportTypes.CSV, {
responseType: 'blob',
query: {
fields:
this.queryParams &&
this.queryParams.fieldsOrder &&
this.queryParams.fieldsOrder.filter(c => this.queryParams.showFields[c]),
offset,
sortArrJson: JSON.stringify(
this.reqPayload &&
this.reqPayload.sorts &&
this.reqPayload.sorts.map(({ fk_column_id, direction }) => ({
direction,
fk_column_id,
}))
),
filterArrJson: JSON.stringify(this.reqPayload && this.reqPayload.filters),
},
headers: {
'xc-password': this.reqPayload && this.reqPayload.password,
},
});
*/
} else {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
selectedView?.value.title as string,
ExportTypes.CSV,
{
responseType: 'blob',
query: {
offset,
},
} as any,
)
}
const { data } = res
function openWebhookModal() { offset = +res.headers['nc-export-offset']
showWebhookDrawer.value = true const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`)
if (offset > -1) {
toast.info('Downloading more files')
} else {
toast.success('Successfully exported all table data')
}
}
} catch (e) {
toast.error(extractSdkResponseErrorMsg(e))
}
} }
</script> </script>
<template> <template>
<a-menu offset-y transition="slide-y-transition" mode="horizontal"> <a-dropdown>
<a-sub-menu key="sub1"> <a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<template #icon> <div class="flex gap-1 align-center">
<setting-outlined /> <MdiFlashIcon class="text-grey" />
</template> <!-- More -->
<template #title> {{ $t('general.more') }}
<span class="flex items-center gap-2"> <MdiMenuDownIcon class="text-grey" />
<MdiFlashOutlineIcon /> </div>
{{ $t('general.more') }} </a-button>
</span> <template #overlay>
</template> <div class="bg-white shadow">
<a-menu-item key="action:downloadCSV" v-t="['c:actions']" @click="exportCsv"> <div>
<span class="flex items-center gap-2"> <div class="nc-menu-item" @click.stop="exportCsv">
<MdiDownloadOutlineIcon class="text-primary" /> <MdiDownloadIcon />
<!-- Download as CSV --> <!-- Download as CSV -->
{{ $t('activity.downloadCSV') }} {{ $t('activity.downloadCSV') }}
</span> </div>
</a-menu-item> <div class="nc-menu-item" @click.stop>
<MdiUploadIcon />
<a-menu-item <!-- Upload CSV -->
v-if="isUIAllowed('csvImport') && !isView" {{ $t('activity.uploadCSV') }}
key="action:uploadCSV" </div>
v-t="['a:actions:upload-csv']" <div class="nc-menu-item" @click.stop>
@click="importCsv" <MdiViewListIcon />
> <!-- Shared View List -->
<span class="flex items-center gap-2"> {{ $t('activity.listSharedView') }}
<MdiUploadOutlineIcon class="text-primary" /> </div>
<!-- Upload CSV --> <div class="nc-menu-item" @click.stop>
{{ $t('activity.uploadCSV') }} <MdiHookIcon />
</span> <!-- todo: i18n -->
</a-menu-item> Webhook
</div>
<a-menu-item </div>
v-if="isUIAllowed('SharedViewList') && !isView" </div>
key="action:listSharedView" </template>
v-t="['a:actions:shared-view-list']" </a-dropdown>
@click="openSharedViewModal"
>
<span class="flex items-center gap-2">
<MdiViewListOutlineIcon class="text-primary" />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('webhook') && !isView"
key="action:webhooks"
v-t="['c:actions:webhook']"
@click="openWebhookModal"
>
<span class="flex items-center gap-2">
<MdiHookIcon class="text-primary" />
<!-- TODO: i18n -->
Webhooks
</span>
</a-menu-item>
</a-sub-menu>
</a-menu>
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
</template> </template>
<style scoped></style>

11
packages/nc-gui-v2/components/smartsheet-toolbar/Reload.vue

@ -0,0 +1,11 @@
<script setup lang="ts">
import { ReloadViewDataHookInj } from '~/context'
import MdiReloadIcon from '~icons/mdi/reload'
const reloadTri = inject(ReloadViewDataHookInj)
</script>
<template>
<MdiReloadIcon class="text-grey" @click="reloadTri.trigger()" />
</template>
<style scoped></style>

37
packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue

@ -0,0 +1,37 @@
<script lang="ts" setup>
import { MetaInj } from '~/context'
const { modelValue, field } = defineProps<{
modelValue?: string
field?: any
}>()
const emit = defineEmits(['update:modelValue', 'update:field'])
const localValue = computed({
get: () => modelValue,
set: (val) => emit('update:modelValue', val),
})
const localField = computed({
get: () => field,
set: (val) => emit('update:field', val),
})
const meta = inject(MetaInj)
const columns = computed(() =>
meta?.value?.columns?.map((c) => ({
value: c.id,
label: c.title,
})),
)
</script>
<template>
<a-input v-model:value="localValue" size="small" class="max-w-[250px]" placeholder="Filter query">
<template #addonBefore>
<a-select v-model:value="localField" :options="columns" style="width: 80px" class="!text-xs" size="small" />
</template>
</a-input>
</template>
<style scoped></style>

18
packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue

@ -0,0 +1,18 @@
<script lang="ts" setup>
import MdiOpenInNew from '~icons/mdi/open-in-new'
const { isUIAllowed } = useUIPermission()
</script>
<template>
<div>
<a-button v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn" size="small">
<div class="flex align-center gap-1">
<MdiOpenInNew class="text-grey" />
<!-- Share View -->
{{ $t('activity.shareView') }}
</div>
</a-button>
</div>
</template>
<style scoped />

115
packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue

@ -6,6 +6,7 @@ import useViewSorts from '~/composables/useViewSorts'
import MdiMenuDownIcon from '~icons/mdi/menu-down' import MdiMenuDownIcon from '~icons/mdi/menu-down'
import MdiSortIcon from '~icons/mdi/sort' import MdiSortIcon from '~icons/mdi/sort'
import MdiDeleteIcon from '~icons/mdi/close-box' import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
const meta = inject(MetaInj) const meta = inject(MetaInj)
const view = inject(ActiveViewInj) const view = inject(ActiveViewInj)
@ -26,68 +27,64 @@ watch(
</script> </script>
<template> <template>
<v-menu offset-y transition="slide-y-transition"> <a-dropdown offset-y class="" :trigger="['click']">
<template #activator="{ props }"> <v-badge :value="sorts && sorts.length" color="primary" dot overlap>
<v-badge :value="sorts && sorts.length" color="primary" dot overlap> <a-button v-t="['c:sort']" size="small" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
<v-btn ><div class="flex align-center gap-1">
v-t="['c:sort']" <MdiSortIcon class="text-grey" />
class="nc-sort-menu-btn px-2 nc-remove-border"
:disabled="isLocked"
small
text
outlined
:class="{
'primary lighten-5 grey&#45;&#45;text text&#45;&#45;darken-3': sorts && sorts.length,
}"
v-bind="props"
>
<MdiSortIcon class="mr-1 text-grey" />
<!-- Sort --> <!-- Sort -->
<span class="text-capitalize">{{ $t('activity.sort') }}</span> <span class="text-capitalize nc-sort-menu-btn">{{ $t('activity.sort') }}</span>
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDownIcon class="text-grey" />
</v-btn> </div>
</v-badge> </a-button>
</template> </v-badge>
<div class="backgroundColor pa-2 menu-filter-dropdown bg-background min-w-[400px]"> <template #overlay>
<div class="sort-grid" @click.stop> <div class="bg-white shadow p-2 menu-filter-dropdown min-w-[400px]">
<template v-for="(sort, i) in sorts || []" :key="i"> <div class="sort-grid" @click.stop>
<!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> --> <template v-for="(sort, i) in sorts || []" :key="i">
<MdiDeleteIcon <!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> -->
class="nc-sort-item-remove-btn text-grey align-self-center" <MdiDeleteIcon
small class="nc-sort-item-remove-btn text-grey align-self-center"
@click.stop="deleteSort(sort, i)" small
></MdiDeleteIcon> @click.stop="deleteSort(sort, i)"
<FieldListAutoCompleteDropdown ></MdiDeleteIcon>
v-model="sort.fk_column_id" <FieldListAutoCompleteDropdown
class="caption nc-sort-field-select" v-model="sort.fk_column_id"
:columns="columns" class="caption nc-sort-field-select"
@click.stop :columns="columns"
@update:model-value="saveOrUpdate(sort, i)" @click.stop
/> @update:model-value="saveOrUpdate(sort, i)"
<v-select />
v-model="sort.direction" <a-select
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select" v-model:value="sort.direction"
:items="['asc', 'desc']" class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select"
:label="$t('labels.operation')" :items="[
density="compact" { text: 'asc', value: 'asc' },
variant="solo" { text: 'desc', value: 'desc' },
hide-details ]"
@click.stop :label="$t('labels.operation')"
@update:model-value="saveOrUpdate(sort, i)" density="compact"
/> variant="solo"
<!-- <template #item="{ item }"> --> hide-details
<!-- <span class="caption font-weight-regular">{{ item.text }}</span> --> @click.stop
<!-- </template> --> @update:model-value="saveOrUpdate(sort, i)"
<!-- </v-select> --> />
</template> <!-- <template #item="{ item }"> -->
<!-- <span class="caption font-weight-regular">{{ item.text }}</span> -->
<!-- </template> -->
<!-- </v-select> -->
</template>
</div>
<a-button size="small" class="text-grey text-capitalize text-sm my-3" @click.stop="addSort">
<div class="flex gap-1 align-center">
<MdiAddIcon />
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</div>
</a-button>
</div> </div>
<v-btn small class="elevation-0 text-grey text-capitalize text-sm my-3" @click.stop="addSort"> </template>
<!-- todo: <v-icon small color="grey"> mdi-plus </v-icon> --> </a-dropdown>
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</v-btn>
</div>
</v-menu>
</template> </template>
<style scoped> <style scoped>

14
packages/nc-gui-v2/components/smartsheet-toolbar/ToggleDrawer.vue

@ -0,0 +1,14 @@
<script setup lang="ts">
import { ReloadViewDataHookInj } from '~/context'
import MdiDoorOpenIcon from '~icons/mdi/door-open'
import MdiDoorClosedIcon from '~icons/mdi/door-closed'
const navDrawerOpened = ref(false)
const Icon = computed(() => (navDrawerOpened.value ? MdiDoorOpenIcon : MdiDoorClosedIcon))
</script>
<template>
<Icon class="text-grey" @click="navDrawerOpened = !navDrawerOpened" />
</template>
<style scoped></style>

252
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -50,9 +50,9 @@ onKeyStroke(['Enter'], (e) => {
}) })
watch( watch(
[() => meta?.value?.id, () => view?.value?.id], () => view?.value?.id,
async (n: any, o: any) => { async (n?: string, o?: string) => {
if (meta?.value && view?.value) { if (n && n !== o) {
await loadData() await loadData()
} }
}, },
@ -65,129 +65,131 @@ defineExpose({
</script> </script>
<template> <template>
<div class="nc-grid-wrapper"> <div class="flex flex-col h-100 min-h-0 w-100">
<table class="xc-row-table nc-grid backgroundColorDefault"> <div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-primary">
<thead> <table class="xc-row-table nc-grid backgroundColorDefault">
<tr> <thead>
<th>#</th> <tr>
<th v-for="col in fields" :key="col.title"> <th>#</th>
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" /> <th v-for="col in fields" :key="col.title">
<SmartsheetHeaderCell v-else :column="col" /> <SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
</th> <SmartsheetHeaderCell v-else :column="col" />
</tr> </th>
</thead> </tr>
<tbody> </thead>
<tr v-for="({ row }, rowIndex) in data" :key="rowIndex" class="nc-grid-row"> <tbody>
<td key="row-index" style="width: 65px" class="caption nc-grid-cell"> <tr v-for="({ row }, rowIndex) in data" :key="rowIndex" class="nc-grid-row">
<div class="d-flex align-center"> <td key="row-index" style="width: 65px" class="caption nc-grid-cell">
{{ rowIndex + 1 }} <div class="d-flex align-center">
</div> {{ rowIndex + 1 }}
</td> </div>
<td </td>
v-for="(columnObj, colIndex) in fields" <td
:key="rowIndex + columnObj.title" v-for="(columnObj, colIndex) in fields"
class="cell pointer nc-grid-cell" :key="rowIndex + columnObj.title"
:class="{ class="cell pointer nc-grid-cell"
active: !isPublicView && selected.col === colIndex && selected.row === rowIndex, :class="{
// 'primary-column': primaryValueColumn === columnObj.title, active: !isPublicView && selected.col === colIndex && selected.row === rowIndex,
// 'text-center': isCentrallyAligned(columnObj), // 'primary-column': primaryValueColumn === columnObj.title,
// 'required': isRequired(columnObj, rowObj), // 'text-center': isCentrallyAligned(columnObj),
}" // 'required': isRequired(columnObj, rowObj),
:data-col="columnObj.title" }"
@click="selectCell(rowIndex, colIndex)" :data-col="columnObj.title"
@dblclick="editEnabled = true" @click="selectCell(rowIndex, colIndex)"
> @dblclick="editEnabled = true"
<!-- @contextmenu=" --> >
<!-- showRowContextMenu($event, rowObj, rowMeta, row, col, columnObj) --> <!-- @contextmenu=" -->
<!-- " --> <!-- showRowContextMenu($event, rowObj, rowMeta, row, col, columnObj) -->
<!-- > --> <!-- " -->
<!-- <virtual-cell --> <!-- > -->
<!-- v-if="isVirtualCol(columnObj)" --> <!-- <virtual-cell -->
<!-- :password="password" --> <!-- v-if="isVirtualCol(columnObj)" -->
<!-- :is-public="isPublicView" --> <!-- :password="password" -->
<!-- :metas="metas" --> <!-- :is-public="isPublicView" -->
<!-- :is-locked="isLocked" --> <!-- :metas="metas" -->
<!-- :column="columnObj" --> <!-- :is-locked="isLocked" -->
<!-- :row="rowObj" --> <!-- :column="columnObj" -->
<!-- :nodes="nodes" --> <!-- :row="rowObj" -->
<!-- :meta="meta" --> <!-- :nodes="nodes" -->
<!-- :api="api" --> <!-- :meta="meta" -->
<!-- :active="selected.col === col && selected.row === row" --> <!-- :api="api" -->
<!-- :sql-ui="sqlUi" --> <!-- :active="selected.col === col && selected.row === row" -->
<!-- :is-new="rowMeta.new" --> <!-- :sql-ui="sqlUi" -->
<!-- v-on="$listeners" --> <!-- :is-new="rowMeta.new" -->
<!-- @updateCol=" --> <!-- v-on="$listeners" -->
<!-- (...args) => --> <!-- @updateCol=" -->
<!-- updateCol( --> <!-- (...args) => -->
<!-- ...args, --> <!-- updateCol( -->
<!-- columnObj.bt --> <!-- ...args, -->
<!-- && meta.columns.find( --> <!-- columnObj.bt -->
<!-- (c) => c.column_name === columnObj.bt.column_name, --> <!-- && meta.columns.find( -->
<!-- ), --> <!-- (c) => c.column_name === columnObj.bt.column_name, -->
<!-- col, --> <!-- ), -->
<!-- row, --> <!-- col, -->
<!-- ) --> <!-- row, -->
<!-- " --> <!-- ) -->
<!-- @saveRow="onCellValueChange(col, row, columnObj, true)" --> <!-- " -->
<!-- /> --> <!-- @saveRow="onCellValueChange(col, row, columnObj, true)" -->
<!-- /> -->
<!-- <editable-cell -->
<!-- v-else-if=" --> <!-- <editable-cell -->
<!-- ((isPkAvail || rowMeta.new) --> <!-- v-else-if=" -->
<!-- && !isView --> <!-- ((isPkAvail || rowMeta.new) -->
<!-- && !isLocked --> <!-- && !isView -->
<!-- && !isPublicView --> <!-- && !isLocked -->
<!-- && editEnabled.col === col --> <!-- && !isPublicView -->
<!-- && editEnabled.row === row) --> <!-- && editEnabled.col === col -->
<!-- || enableEditable(columnObj) --> <!-- && editEnabled.row === row) -->
<!-- " --> <!-- || enableEditable(columnObj) -->
<!-- v-model="rowObj[columnObj.title]" --> <!-- " -->
<!-- :column="columnObj" --> <!-- v-model="rowObj[columnObj.title]" -->
<!-- :meta="meta" --> <!-- :column="columnObj" -->
<!-- :active="selected.col === col && selected.row === row" --> <!-- :meta="meta" -->
<!-- :sql-ui="sqlUi" --> <!-- :active="selected.col === col && selected.row === row" -->
<!-- :db-alias="nodes.dbAlias" --> <!-- :sql-ui="sqlUi" -->
<!-- :is-locked="isLocked" --> <!-- :db-alias="nodes.dbAlias" -->
<!-- :is-public="isPublicView" --> <!-- :is-locked="isLocked" -->
<!-- :view-id="viewId" --> <!-- :is-public="isPublicView" -->
<!-- @save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);" --> <!-- :view-id="viewId" -->
<!-- @cancel="editEnabled = {}" --> <!-- @save="editEnabled = {}; onCellValueChange(col, row, columnObj, true);" -->
<!-- @update="onCellValueChange(col, row, columnObj, false)" --> <!-- @cancel="editEnabled = {}" -->
<!-- @blur="onCellValueChange(col, row, columnObj, true)" --> <!-- @update="onCellValueChange(col, row, columnObj, false)" -->
<!-- @input="unsaved = true" --> <!-- @blur="onCellValueChange(col, row, columnObj, true)" -->
<!-- @navigateToNext="navigateToNext" --> <!-- @input="unsaved = true" -->
<!-- @navigateToPrev="navigateToPrev" --> <!-- @navigateToNext="navigateToNext" -->
<!-- /> --> <!-- @navigateToPrev="navigateToPrev" -->
<!-- /> -->
<SmartsheetVirtualCell v-if="isVirtualCol(columnObj)" v-model="row[columnObj.title]" :column="columnObj" />
<SmartsheetVirtualCell v-if="isVirtualCol(columnObj)" v-model="row[columnObj.title]" :column="columnObj" />
<SmartsheetCell
v-else <SmartsheetCell
v-model="row[columnObj.title]" v-else
:column="columnObj" v-model="row[columnObj.title]"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex" :column="columnObj"
@update:model-value="updateRowProperty(row, columnObj.title)" :edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
/> @update:model-value="updateRowProperty(row, columnObj.title)"
/>
<!-- <SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" /> -->
<!-- :selected="selected.col === col && selected.row === row" --> <!-- <SmartsheetCell v-else :column="columnObj" :value="row[columnObj.title]" /> -->
<!-- :is-locked="isLocked" --> <!-- :selected="selected.col === col && selected.row === row" -->
<!-- :column="columnObj" --> <!-- :is-locked="isLocked" -->
<!-- :meta="meta" --> <!-- :column="columnObj" -->
<!-- :db-alias="nodes.dbAlias" --> <!-- :meta="meta" -->
<!-- :value="rowObj[columnObj.title]" --> <!-- :db-alias="nodes.dbAlias" -->
<!-- :sql-ui="sqlUi" --> <!-- :value="rowObj[columnObj.title]" -->
<!-- @enableedit=" --> <!-- :sql-ui="sqlUi" -->
<!-- makeSelected(col, row); --> <!-- @enableedit=" -->
<!-- makeEditable(col, row, columnObj.ai, rowMeta); --> <!-- makeSelected(col, row); -->
<!-- " --> <!-- makeEditable(col, row, columnObj.ai, rowMeta); -->
<!-- /> --> <!-- " -->
</td> <!-- /> -->
</tr> </td>
</tbody> </tr>
</table> </tbody>
</table>
</div>
<SmartsheetPagination />
</div> </div>
<SmartsheetPagination />
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">

20
packages/nc-gui-v2/components/smartsheet/Pagination.vue

@ -48,7 +48,7 @@ export default {
<div class="d-flex align-center"> <div class="d-flex align-center">
<span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span> <span v-if="count !== null && count !== Infinity" class="caption ml-2"> {{ count }} record{{ count !== 1 ? 's' : '' }} </span>
<v-spacer /> <v-spacer />
<v-pagination <!-- <v-pagination
v-if="count !== Infinity" v-if="count !== Infinity"
v-model="page" v-model="page"
style="max-width: 100%" style="max-width: 100%"
@ -56,6 +56,17 @@ export default {
:total-visible="8" :total-visible="8"
color="primary lighten-2" color="primary lighten-2"
class="nc-pagination" class="nc-pagination"
/> -->
<a-pagination
v-if="count !== Infinity"
v-model:current="page"
size="small"
class="!text-xs !m-1"
:total="count"
:page-size="size"
show-less-items
:show-size-changer="false"
/> />
<div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px"> <div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px">
<span class="caption" style="white-space: nowrap"> Change page:</span> <span class="caption" style="white-space: nowrap"> Change page:</span>
@ -79,3 +90,10 @@ export default {
<v-spacer /> <v-spacer />
</div> </div>
</template> </template>
<style scoped>
:deep(.ant-pagination-item a) {
line-height: 21px !important;
@apply text-sm;
}
</style>

25
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -1,12 +1,31 @@
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template> <template>
<v-toolbar dense class="nc-table-toolbar elevation-0 xc-toolbar xc-border-bottom" style="z-index: 7"> <div dense class="nc-table-toolbar w-100 p-1 flex gap-1 align-center" style="z-index: 7">
<SmartsheetToolbarSearchData class="flex-shrink" />
<SmartsheetToolbarFieldsMenu :show-system-fields="false" /> <SmartsheetToolbarFieldsMenu :show-system-fields="false" />
<SmartsheetToolbarColumnFilterMenu /> <SmartsheetToolbarColumnFilterMenu />
<SmartsheetToolbarSortListMenu /> <SmartsheetToolbarSortListMenu />
<SmartsheetToolbarShareView />
<SmartsheetToolbarMoreActions /> <SmartsheetToolbarMoreActions />
</v-toolbar> <div class="flex-1" />
<SmartsheetToolbarLockMenu />
<div class="dot" />
<SmartsheetToolbarReload />
<div class="dot" />
<SmartsheetToolbarAddRow />
<div class="dot" />
<SmartsheetToolbarDeleteTable />
<div class="dot" />
<SmartsheetToolbarToggleDrawer class="mr-2" />
</div>
</template> </template>
<style scoped></style> <style scoped>
:deep(.nc-toolbar-btn) {
@apply border-0 !text-xs font-semibold px-2;
}
.dot {
@apply w-[3px] h-[3px] bg-gray-300 mx-1 rounded-full;
}
</style>

43
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -1,25 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { useEventBus } from '@vueuse/core' import type { ColumnType, ViewType } from 'nocodb-sdk'
import type { ColumnType, FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { computed, onMounted, provide, watch } from '#imports' import { computed, inject, onMounted, provide, watch, watchEffect } from '#imports'
import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj, TabMetaInj } from '~/context' import { ActiveViewInj, FieldsInj, IsLockedInj, MetaInj, ReloadViewDataHookInj, TabMetaInj } from '~/context'
import useMetas from '~/composables/useMetas' import useMetas from '~/composables/useMetas'
const { tabMeta } = defineProps({
tabMeta: Object,
})
const { getMeta, metas } = useMetas() const { getMeta, metas } = useMetas()
const activeView = ref<GridType | FormType | KanbanType | GalleryType>() const activeView = ref<ViewType>()
const el = ref<any>() const el = ref<any>()
const fields = ref<ColumnType[]>([]) const fields = ref<ColumnType[]>([])
const meta = computed(() => metas.value?.[tabMeta?.id]) const tabMeta = inject(TabMetaInj)
const meta = computed(() => metas.value?.[tabMeta?.value?.id as string])
onMounted(async () => { watchEffect(async () => {
await getMeta(tabMeta?.id) await getMeta(tabMeta?.value?.id as string)
}) })
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
@ -40,17 +37,19 @@ watch(
</script> </script>
<template> <template>
<div class="overflow-auto"> <div class="nc-container flex h-full">
<SmartsheetToolbar /> <div class="flex flex-col h-full flex-1 min-w-0">
<template v-if="meta"> <SmartsheetToolbar />
<div class="d-flex"> <template v-if="meta">
<div v-if="activeView" class="flex-grow-1 min-w-0"> <div class="flex flex-1 min-h-0">
<SmartsheetGrid v-if="activeView.type === ViewTypes.GRID" :ref="el" /> <div v-if="activeView" class="h-full flex-grow min-w-0 min-h-0">
<SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" /> <SmartsheetGrid v-if="activeView.type === ViewTypes.GRID" :ref="el" />
<SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" /> <SmartsheetGallery v-else-if="activeView.type === ViewTypes.GALLERY" />
<SmartsheetForm v-else-if="activeView.type === ViewTypes.FORM" />
</div>
<SmartsheetSidebar />
</div> </div>
<SmartsheetSidebar /> </template>
</div> </div>
</template>
</div> </div>
</template> </template>

1
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -7,6 +7,7 @@ import useBelongsTo from '~/composables/useBelongsTo'
const column = inject(ColumnInj) const column = inject(ColumnInj)
const value = inject('value') const value = inject('value')
const active = false const active = false
const localState = null
const { parentMeta, loadParentMeta, primaryValueProp } = useBelongsTo(column as ColumnType) const { parentMeta, loadParentMeta, primaryValueProp } = useBelongsTo(column as ColumnType)
await loadParentMeta() await loadParentMeta()

1
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -6,6 +6,7 @@ import useManyToMany from '~/composables/useManyToMany'
const column = inject(ColumnInj) const column = inject(ColumnInj)
const value = inject('value') const value = inject('value')
const active = false const active = false
const isLocked = false
const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType) const { childMeta, loadChildMeta, primaryValueProp } = useManyToMany(column as ColumnType)
await loadChildMeta() await loadChildMeta()

79
packages/nc-gui-v2/composables/useTabs.ts

@ -1,7 +1,15 @@
import type { WritableComputedRef } from '@vue/reactivity'
import { useState } from '#app' import { useState } from '#app'
import useProject from '~/composables/useProject'
enum TabType {
TABLE = 'table',
VIEW = 'view',
AUTH = 'auth',
}
export interface TabItem { export interface TabItem {
type: 'table' | 'view' | 'auth' type: TabType
title: string title: string
id?: string id?: string
} }
@ -15,30 +23,79 @@ function getPredicate(key: Partial<TabItem>) {
export default () => { export default () => {
const tabs = useState<TabItem[]>('tabs', () => []) const tabs = useState<TabItem[]>('tabs', () => [])
const activeTab = useState<number>('activeTab', () => 0) // const activeTab = useState<number>('activeTab', () => 0)
const route = useRoute()
const router = useRouter()
const { tables } = useProject()
const activeTabIndex: WritableComputedRef<number> = computed({
get() {
console.log(route?.name)
if ((route?.name as string)?.startsWith('nc-projectId-index-index-type-title-viewTitle') && tables?.value?.length) {
const tab: Partial<TabItem> = { type: route.params.type as TabType, title: route.params.title as string }
const id = tables?.value?.find((t) => t.title === tab.title)?.id
tab.id = id as string
let index = tabs.value.findIndex((t) => t.id === tab.id)
if (index === -1) {
tabs.value.push(tab as TabItem)
index = tabs.value.length - 1
}
return index
} else if ((route?.name as string)?.startsWith('nc-projectId-index-index-auth')) {
return tabs.value.findIndex((t) => t.type === 'auth')
}
return -1
},
set(index: number) {
if (index === -1) {
router.push(`/nc/${route.params.projectId}`)
} else {
const tab = tabs.value[index]
if (!tab) {
return
}
if (tab.type === TabType.TABLE) {
router.push(`/nc/${route.params.projectId}/table/${tab?.title}`)
} else if (tab.type === TabType.VIEW) {
router.push(`/nc/${route.params.projectId}/view/${tab?.title}`)
} else if (tab.type === TabType.AUTH) {
router.push(`/nc/${route.params.projectId}/auth`)
}
}
},
})
const activeTab = computed(() => tabs.value?.[activeTabIndex.value])
const addTab = (tabMeta: TabItem) => { const addTab = (tabMeta: TabItem) => {
const tabIndex = tabs.value.findIndex((tab) => tab.id === tabMeta.id) const tabIndex = tabs.value.findIndex((tab) => tab.id === tabMeta.id)
// if tab already found make it active // if tab already found make it active
if (tabIndex > -1) { if (tabIndex > -1) {
activeTab.value = tabIndex activeTabIndex.value = tabIndex
} }
// if tab not found add it // if tab not found add it
else { else {
tabs.value = [...(tabs.value || []), tabMeta] tabs.value = [...(tabs.value || []), tabMeta]
activeTab.value = tabs.value.length - 1 activeTabIndex.value = tabs.value.length - 1
} }
} }
const clearTabs = () => { const clearTabs = () => {
tabs.value = [] tabs.value = []
} }
const closeTab = async (key: number | Partial<TabItem>) => {
const closeTab = (key: number | Partial<TabItem>) => { const index = typeof key === 'number' ? key : tabs.value.findIndex(getPredicate(key))
if (typeof key === 'number') tabs.value.splice(key, 1) if (activeTabIndex.value === index) {
else { let newTabIndex = index - 1
const index = tabs.value.findIndex(getPredicate(key)) if (newTabIndex < 0 && tabs.value?.length > 1) newTabIndex = index + 1
if (index > -1) tabs.value.splice(index, 1) if (newTabIndex === -1) {
await router.push(`/nc/${route.params.projectId}`)
} else {
await router.push(`/nc/${route.params.projectId}/table/${tabs.value?.[newTabIndex]?.title}`)
}
} }
tabs.value.splice(index, 1)
} }
const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => { const updateTab = (key: number | Partial<TabItem>, newTabItemProps: Partial<TabItem>) => {
@ -48,5 +105,5 @@ export default () => {
} }
} }
return { tabs, addTab, activeTab, clearTabs, closeTab, updateTab } return { tabs, addTab, activeTabIndex, activeTab, clearTabs, closeTab, updateTab }
} }

5
packages/nc-gui-v2/composables/useViewColumns.ts

@ -119,6 +119,10 @@ export default function (
?.sort((c1, c2) => c1.order - c2.order) ?.sort((c1, c2) => c1.order - c2.order)
?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) || []) as ColumnType[] ?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) || []) as ColumnType[]
}) })
const sortedFields = computed<ColumnType[]>(() => {
return (fields?.value?.sort((c1, c2) => c1.order - c2.order)?.map((c) => metaColumnById?.value?.[c.fk_column_id as string]) ||
[]) as ColumnType[]
})
return { return {
fields, fields,
@ -130,5 +134,6 @@ export default function (
saveOrUpdate, saveOrUpdate,
sortedAndFilteredFields, sortedAndFilteredFields,
showSystemFields, showSystemFields,
sortedFields,
} }
} }

5
packages/nc-gui-v2/context/index.ts

@ -1,11 +1,12 @@
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import type { InjectionKey, Ref } from 'vue' import type { ComputedRef, InjectionKey, Ref } from 'vue'
import type { EventHook } from '@vueuse/core' import type { EventHook } from '@vueuse/core'
import type { useViewData } from '#imports' import type { useViewData } from '#imports'
import type { TabItem } from '~/composables/useTabs'
export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection') export const ColumnInj: InjectionKey<ColumnType & { meta: any }> = Symbol('column-injection')
export const MetaInj: InjectionKey<Ref<TableType>> = Symbol('meta-injection') export const MetaInj: InjectionKey<Ref<TableType>> = Symbol('meta-injection')
export const TabMetaInj: InjectionKey<any> = Symbol('tab-meta-injection') export const TabMetaInj: InjectionKey<ComputedRef<TabItem>> = Symbol('tab-meta-injection')
export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> = export const PaginationDataInj: InjectionKey<ReturnType<typeof useViewData>['paginationData']> =
Symbol('pagination-data-injection') Symbol('pagination-data-injection')
export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection') export const ChangePageInj: InjectionKey<ReturnType<typeof useViewData>['changePage']> = Symbol('pagination-data-injection')

2
packages/nc-gui-v2/nuxt.config.ts

@ -49,7 +49,7 @@ export default defineNuxtConfig({
css: { css: {
preprocessorOptions: { preprocessorOptions: {
less: { less: {
modifyVars: { 'primary-color': '#1348ba' }, modifyVars: { 'primary-color': '#1348ba', 'text-color': 'rgba(61, 61, 61, 1)' },
javascriptEnabled: true, javascriptEnabled: true,
}, },
}, },

43
packages/nc-gui-v2/package-lock.json generated

@ -9,6 +9,7 @@
"@vueuse/integrations": "^8.9.1", "@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6", "ant-design-vue": "^3.1.0-rc.6",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
@ -18,6 +19,7 @@
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10", "vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13", "vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3" "xlsx": "^0.17.3"
}, },
@ -15684,6 +15686,11 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"node_modules/file-uri-to-path": { "node_modules/file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"dev": true, "dev": true,
@ -21953,6 +21960,22 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"dependencies": {
"sortablejs": "1.14.0"
},
"peerDependencies": {
"vue": "^3.0.1"
}
},
"node_modules/vuedraggable/node_modules/sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
},
"node_modules/vuetify": { "node_modules/vuetify": {
"version": "3.0.0-beta.5", "version": "3.0.0-beta.5",
"license": "MIT", "license": "MIT",
@ -26412,6 +26435,11 @@
"flat-cache": "^3.0.4" "flat-cache": "^3.0.4"
} }
}, },
"file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"file-uri-to-path": { "file-uri-to-path": {
"version": "1.0.0", "version": "1.0.0",
"dev": true "dev": true
@ -36450,6 +36478,21 @@
"is-plain-object": "3.0.1" "is-plain-object": "3.0.1"
} }
}, },
"vuedraggable": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
"requires": {
"sortablejs": "1.14.0"
},
"dependencies": {
"sortablejs": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
}
}
},
"vuetify": { "vuetify": {
"version": "3.0.0-beta.5", "version": "3.0.0-beta.5",
"requires": {} "requires": {}

6
packages/nc-gui-v2/package.json

@ -15,6 +15,7 @@
"@vueuse/integrations": "^8.9.1", "@vueuse/integrations": "^8.9.1",
"ant-design-vue": "^3.1.0-rc.6", "ant-design-vue": "^3.1.0-rc.6",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"monaco-editor": "^0.33.0", "monaco-editor": "^0.33.0",
"nocodb-sdk": "file:../nocodb-sdk", "nocodb-sdk": "file:../nocodb-sdk",
@ -24,8 +25,9 @@
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"vue-i18n": "^9.1.10", "vue-i18n": "^9.1.10",
"vue-toastification": "^2.0.0-rc.5", "vue-toastification": "^2.0.0-rc.5",
"vuetify": "^3.0.0-alpha.13", "xlsx": "^0.17.3",
"xlsx": "^0.17.3" "vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "^0.25.2", "@antfu/eslint-config": "^0.25.2",

40
packages/nc-gui-v2/pages/nc/[projectId].vue → packages/nc-gui-v2/pages/nc/[projectId]/index.vue

@ -1,10 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import useTabs from '~/composables/useTabs'
const route = useRoute() const route = useRoute()
const { loadProject, loadTables } = useProject(route.params.projectId as string) const { loadProject, loadTables } = useProject(route.params.projectId as string)
const { clearTabs, addTab } = useTabs() const { clearTabs, addTab } = useTabs()
const { $state } = useNuxtApp() const { $state } = useNuxtApp()
addTab({ type: 'auth', title: 'Team & Auth' }) if (!route.params.type) {
addTab({ type: 'auth', title: 'Team & Auth' })
}
watch( watch(
() => route.params.projectId, () => route.params.projectId,
@ -27,38 +31,6 @@ $state.sidebarOpen.value = true
<template #sidebar> <template #sidebar>
<DashboardTreeView /> <DashboardTreeView />
</template> </template>
<NuxtPage />
<v-container fluid>
<DashboardTabView />
</v-container>
</NuxtLayout> </NuxtLayout>
</template> </template>
<style scoped lang="scss">
.nc-container {
.nc-topbar {
position: fixed;
top: 0;
left: 0;
height: 50px;
width: 100%;
z-index: 5;
}
.nc-sidebar {
position: fixed;
top: 50px;
left: 0;
height: calc(100% - 50px);
width: 250px;
}
.nc-content {
position: fixed;
top: 50px;
left: 250px;
height: calc(100% - 50px);
width: calc(100% - 250px);
}
}
</style>

152
packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue

@ -0,0 +1,152 @@
<script setup lang="ts">
import useTabs from '~/composables/useTabs'
import { TabMetaInj } from '~/context'
import useUIPermission from '~/composables/useUIPermission'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiTableIcon from '~icons/mdi/table'
import MdiCsvIcon from '~icons/mdi/file-document-outline'
import MdiExcelIcon from '~icons/mdi/file-excel'
import MdiJSONIcon from '~icons/mdi/code-json'
import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiRequestDataSourceIcon from '~icons/mdi/open-in-new'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
const { tabs, activeTabIndex, activeTab, closeTab } = useTabs()
const { isUIAllowed } = useUIPermission()
const tableCreateDialog = ref(false)
const airtableImportDialog = ref(false)
const quickImportDialog = ref(false)
const importType = ref('')
const currentMenu = ref<string[]>(['addORImport'])
provide(TabMetaInj, activeTab)
function onEdit(targetKey: number, action: string) {
if (action !== 'add') {
closeTab(targetKey)
}
}
function openQuickImportDialog(type: string) {
quickImportDialog.value = true
importType.value = type
}
</script>
<template>
<div class="nc-container d-flex flex-column">
<div>
<a-tabs v-model:activeKey="activeTabIndex" size="small" type="editable-card" @edit="closeTab">
<a-tab-pane v-for="(tab, i) in tabs" :key="i" :tab="tab.title" />
<template #leftExtra>
<a-menu v-model:selectedKeys="currentMenu" mode="horizontal">
<a-sub-menu key="addORImport">
<template #title>
<div class="text-sm flex items-center gap-2">
<MdiPlusIcon />
Add / Import
</div>
</template>
<a-menu-item-group v-if="isUIAllowed('addTable')">
<a-menu-item key="add-new-table" v-t="['a:actions:create-table']" @click="tableCreateDialog = true">
<span class="flex items-center gap-2">
<MdiTableIcon class="text-primary" />
<!-- Add new table -->
{{ $t('tooltip.addTable') }}
</span>
</a-menu-item>
</a-menu-item-group>
<a-menu-item-group title="QUICK IMPORT FROM">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
v-t="['a:actions:import-airtable']"
@click="airtableImportDialog = true"
>
<span class="flex items-center gap-2">
<MdiAirTableIcon class="text-primary" />
<!-- TODO: i18n -->
Airtable
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('csvImport')"
key="quick-import-csv"
v-t="['a:actions:import-csv']"
@click="openQuickImportDialog('csv')"
>
<span class="flex items-center gap-2">
<MdiCsvIcon class="text-primary" />
<!-- TODO: i18n -->
CSV file
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('jsonImport')"
key="quick-import-json"
v-t="['a:actions:import-json']"
@click="openQuickImportDialog('json')"
>
<span class="flex items-center gap-2">
<MdiJSONIcon class="text-primary" />
<!-- TODO: i18n -->
JSON file
</span>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
v-t="['a:actions:import-excel']"
@click="openQuickImportDialog('excel')"
>
<span class="flex items-center gap-2">
<MdiExcelIcon class="text-primary" />
<!-- TODO: i18n -->
Microsoft Excel
</span>
</a-menu-item>
</a-menu-item-group>
<a-divider class="ma-0 mb-2" />
<a-menu-item
v-if="isUIAllowed('importRequest')"
key="add-new-table"
v-t="['e:datasource:import-request']"
class="ma-0 mt-3"
>
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank" class="prose-sm pa-0">
<span class="flex items-center gap-2">
<MdiRequestDataSourceIcon class="text-primary" />
<!-- TODO: i18n -->
Request a data source you need?
</span>
</a>
</a-menu-item>
</a-sub-menu>
</a-menu>
</template>
</a-tabs>
</div>
<div class="flex-1 min-h-0">
<NuxtPage />
</div>
<DlgTableCreate v-if="tableCreateDialog" v-model="tableCreateDialog" />
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" :import-type="importType" />
<DlgAirtableImport v-if="airtableImportDialog" v-model="airtableImportDialog" />
</div>
</template>
<style scoped>
.nc-container {
height: calc(calc(100vh - var(--header-height)));
@apply overflow-hidden;
}
:deep(.ant-tabs-nav) {
@apply !mb-0;
}
</style>

11
packages/nc-gui-v2/pages/nc/[projectId]/index/index/[type]/[title]/[[viewTitle]].vue

@ -0,0 +1,11 @@
<script>
export default {
name: 'Index',
}
</script>
<template>
<TabsSmartsheet />
</template>
<style scoped></style>

5
packages/nc-gui-v2/pages/nc/[projectId]/index/index/auth.vue

@ -0,0 +1,5 @@
<template>
<div>
<h2 class="text-3xl mt-3">Team & Auth</h2>
</div>
</template>

17
packages/nc-gui-v2/pages/nc/[projectId]/index/index/index.vue

@ -0,0 +1,17 @@
<script>
export default {
name: 'Index',
}
</script>
<template>
<div class="nc-main-tab">
<span>Welcome to NocoDB!</span>
</div>
</template>
<style scoped>
.nc-main-tab {
@apply w-full text-3xl text-gray-400 flex align-center justify-center;
}
</style>

1
packages/nc-gui-v2/pages/project/index/create-external.vue

@ -8,7 +8,6 @@ import { navigateTo, useNuxtApp } from '#app'
import { ClientType } from '~/lib/enums' import { ClientType } from '~/lib/enums'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { readFile } from '~/utils/fileUtils' import { readFile } from '~/utils/fileUtils'
import type { ProjectCreateForm } from '~/utils/projectCreateUtils' import type { ProjectCreateForm } from '~/utils/projectCreateUtils'
import { import {
clientTypes, clientTypes,

13
packages/nocodb-sdk/src/lib/helperFunctions.ts

@ -12,12 +12,13 @@ const getSystemColumnsIds = (columns) => {
const getSystemColumns = (columns) => columns.filter(isSystemColumn) || []; const getSystemColumns = (columns) => columns.filter(isSystemColumn) || [];
const isSystemColumn = (col) => const isSystemColumn = (col) =>
col.uidt === UITypes.ForeignKey || col &&
col.column_name === 'created_at' || (col.uidt === UITypes.ForeignKey ||
col.column_name === 'updated_at' || col.column_name === 'created_at' ||
(col.pk && (col.ai || col.cdf)) || col.column_name === 'updated_at' ||
(col.pk && col.meta && col.meta.ag) || (col.pk && (col.ai || col.cdf)) ||
col.system; (col.pk && col.meta && col.meta.ag) ||
col.system);
export { export {
filterOutSystemColumns, filterOutSystemColumns,

Loading…
Cancel
Save