Browse Source

Merge branch 'develop' into feat/gui-v2-sidebar-state

pull/3174/head
Wing-Kam Wong 2 years ago
parent
commit
00d089b76d
  1. 6
      packages/nc-gui-v2/components.d.ts
  2. 10
      packages/nc-gui-v2/components/cell/DatePicker.vue
  3. 12
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  4. 18
      packages/nc-gui-v2/components/cell/Json.vue
  5. 9
      packages/nc-gui-v2/components/cell/MultiSelect.vue
  6. 9
      packages/nc-gui-v2/components/cell/SingleSelect.vue
  7. 9
      packages/nc-gui-v2/components/cell/TimePicker.vue
  8. 10
      packages/nc-gui-v2/components/cell/YearPicker.vue
  9. 2
      packages/nc-gui-v2/components/dashboard/GithubStarButton.vue
  10. 3
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  11. 50
      packages/nc-gui-v2/components/shared-view/AskPassword.vue
  12. 87
      packages/nc-gui-v2/components/shared-view/Form.vue
  13. 32
      packages/nc-gui-v2/components/shared-view/Grid.vue
  14. 2
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  15. 3
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  16. 4
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  17. 4
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  18. 55
      packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue
  19. 8
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  20. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  21. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  22. 38
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  23. 5
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  24. 21
      packages/nc-gui-v2/components/smartsheet/Form.vue
  25. 41
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  26. 10
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  27. 2
      packages/nc-gui-v2/components/smartsheet/VirtualCell.vue
  28. 23
      packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue
  29. 6
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  30. 7
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  31. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  32. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  33. 13
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  34. 12
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  35. 14
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  36. 4
      packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue
  37. 8
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  38. 18
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  39. 14
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  40. 10
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  41. 32
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  42. 10
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  43. 8
      packages/nc-gui-v2/composables/useGridViewColumnWidth.ts
  44. 9
      packages/nc-gui-v2/composables/useLTARStore.ts
  45. 10
      packages/nc-gui-v2/composables/useMetas.ts
  46. 24
      packages/nc-gui-v2/composables/useProject.ts
  47. 108
      packages/nc-gui-v2/composables/useSharedView.ts
  48. 7
      packages/nc-gui-v2/composables/useSmartsheetRowStore.ts
  49. 58
      packages/nc-gui-v2/composables/useViewColumns.ts
  50. 38
      packages/nc-gui-v2/composables/useViewData.ts
  51. 73
      packages/nc-gui-v2/composables/useViewFilters.ts
  52. 47
      packages/nc-gui-v2/composables/useViewSorts.ts
  53. 3
      packages/nc-gui-v2/context/index.ts
  54. 37
      packages/nc-gui-v2/layouts/shared-view.vue
  55. 15
      packages/nc-gui-v2/lib/types.ts
  56. 95
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  57. 39
      packages/nc-gui-v2/pages/[projectType]/form/[viewId].vue
  58. 32
      packages/nc-gui-v2/pages/[projectType]/view/[viewId].vue
  59. 6
      packages/nc-gui-v2/utils/NcAutocompleteTree.ts
  60. 5
      packages/nc-gui-v2/utils/urlUtils.ts

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

@ -61,6 +61,7 @@ declare module '@vue/runtime-core' {
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypography: typeof import('ant-design-vue/es')['Typography']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
@ -105,6 +106,7 @@ declare module '@vue/runtime-core' {
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiCog: typeof import('~icons/mdi/cog')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
@ -125,6 +127,7 @@ declare module '@vue/runtime-core' {
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
@ -135,6 +138,7 @@ declare module '@vue/runtime-core' {
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyPlus: typeof import('~icons/mdi/key-plus')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
@ -153,8 +157,10 @@ declare module '@vue/runtime-core' {
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiSearch: typeof import('~icons/mdi/search')['default']
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']

10
packages/nc-gui-v2/components/cell/DatePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ColumnInj, EditModeInj, computed, inject, ref, watch } from '#imports'
import { ColumnInj, ReadonlyInj, computed, inject, ref, watch } from '#imports'
interface Props {
modelValue?: string | null
@ -12,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const columnMeta = inject(ColumnInj, null)!
const editEnabled = inject(EditModeInj)!
const readOnly = inject(ReadonlyInj, false)
let isDateInvalid = $ref(false)
@ -55,7 +55,7 @@ watch(
{ flush: 'post' },
)
const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : editEnabled.value ? 'Select date' : ''))
const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : ''))
</script>
<template>
@ -65,10 +65,10 @@ const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : editEnabled
class="!w-full px-1"
:format="dateFormat"
:placeholder="placeholder"
:allow-clear="!editEnabled"
:allow-clear="!readOnly"
:input-read-only="true"
:dropdown-class-name="randomClass"
:open="editEnabled ? false : open"
:open="readOnly ? false : open"
@click="open = !open"
>
<template #suffixIcon></template>

12
packages/nc-gui-v2/components/cell/DateTimePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { EditModeInj } from '~/context'
import { ReadonlyInj } from '#imports'
interface Props {
modelValue: string | null | undefined
@ -12,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const editEnabled = inject(EditModeInj)
const readOnly = inject(ReadonlyInj, false)
let isDateInvalid = $ref(false)
@ -64,12 +64,12 @@ watch(
:bordered="false"
class="!w-full px-1"
format="YYYY-MM-DD HH:mm"
:placeholder="isDateInvalid ? 'Invalid date' : !editEnabled ? 'Select date and time' : ''"
:allow-clear="!editEnabled"
:placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly"
:input-read-only="true"
:dropdown-class-name="randomClass"
:open="editEnabled ? false : open"
:disabled="!editEnabled"
:open="readOnly ? false : open"
:disabled="readOnly"
@click="open = !open"
@ok="open = !open"
>

18
packages/nc-gui-v2/components/cell/Json.vue

@ -2,7 +2,7 @@
import { Modal as AModal } from 'ant-design-vue'
import Editor from '~/components/monaco/Editor.vue'
import { ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, IsFormInj } from '~/context'
interface Props {
modelValue: string | Record<string, any> | undefined
@ -18,23 +18,29 @@ const emits = defineEmits<Emits>()
const editEnabled = inject(EditModeInj, ref(false))
const isForm = inject(IsFormInj, ref(false))
const readonly = inject(ReadonlyInj)
const vModel = useVModel(props, 'modelValue', emits)
const localValueState = ref<string | undefined>()
let error = $ref<string | undefined>()
let isExpanded = $ref(false)
const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value,
set: (val: undefined | string | Record<string, any>) => {
localValueState.value = typeof val === 'object' ? JSON.stringify(val, null, 2) : val
/** if form and not expanded then sync directly */
if (isForm.value && !isExpanded) {
vModel.value = val
}
},
})
let error = $ref<string | undefined>()
let isExpanded = $ref(false)
const clear = () => {
error = undefined
@ -98,7 +104,7 @@ watch(editEnabled, () => {
<CilFullscreen v-else class="h-2.5" />
</a-button>
<div class="flex flex-row">
<div v-if="!isForm || isExpanded" class="flex flex-row">
<a-button type="text" size="small" :onclick="clear"><div class="text-xs">Cancel</div></a-button>
<a-button type="primary" size="small" :disabled="!!error || localValue === vModel">

9
packages/nc-gui-v2/components/cell/MultiSelect.vue

@ -4,7 +4,7 @@ import type { SelectOptionsType } from 'nocodb-sdk'
import {
ActiveCellInj,
ColumnInj,
EditModeInj,
ReadonlyInj,
computed,
h,
inject,
@ -28,7 +28,7 @@ const { isMysql } = useProject()
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)!
const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false))
@ -129,12 +129,11 @@ watch(isOpen, (n, _o) => {
v-model:value="vModel"
mode="multiple"
class="w-full"
placeholder="Select an option"
:bordered="false"
show-arrow
:show-arrow="!readOnly"
:show-search="false"
:open="isOpen"
:disabled="!editEnabled"
:disabled="readOnly"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>

9
packages/nc-gui-v2/components/cell/SingleSelect.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import type { Select as AntSelect } from 'ant-design-vue'
import type { SelectOptionsType } from 'nocodb-sdk'
import { ActiveCellInj, ColumnInj, EditModeInj, computed, inject, ref, useEventListener, watch } from '#imports'
import { ActiveCellInj, ColumnInj, ReadonlyInj, computed, inject, ref, useEventListener, watch } from '#imports'
interface Props {
modelValue?: string | undefined
@ -13,7 +13,7 @@ const emit = defineEmits(['update:modelValue'])
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)!
const readOnly = inject(ReadonlyInj)!
const active = inject(ActiveCellInj, ref(false))
@ -69,11 +69,10 @@ watch(isOpen, (n, _o) => {
v-model:value="vModel"
class="w-full"
:allow-clear="!column.rqd && active"
placeholder="Select an option"
:bordered="false"
:open="isOpen"
:disabled="!editEnabled"
:show-arrow="active || vModel === null"
:disabled="readOnly"
:show-arrow="!readOnly && (active || vModel === null)"
@select="isOpen = false"
@keydown="handleKeys"
@click="isOpen = !isOpen"

9
packages/nc-gui-v2/components/cell/TimePicker.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { EditModeInj } from '~/context'
interface Props {
modelValue?: string | null | undefined
@ -13,7 +12,7 @@ const emit = defineEmits(['update:modelValue'])
const { isMysql } = useProject()
const editEnabled = inject(EditModeInj)
const readOnly = inject(ReadonlyInj, false)
let isTimeInvalid = $ref(false)
@ -76,10 +75,10 @@ watch(
use12-hours
format="HH:mm"
class="!w-full px-1"
:placeholder="isTimeInvalid ? 'Invalid time' : !readOnlyMode ? 'Select time' : ''"
:allow-clear="!editEnabled"
:placeholder="isTimeInvalid ? 'Invalid time' : ''"
:allow-clear="!readOnly"
:input-read-only="true"
:open="editEnabled ? false : open"
:open="readOnly ? false : open"
:popup-class-name="randomClass"
@click="open = !open"
@ok="open = !open"

10
packages/nc-gui-v2/components/cell/YearPicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { EditModeInj, computed, inject, onClickOutside, ref, watch } from '#imports'
import { computed, inject, onClickOutside, ref, watch } from '#imports'
interface Props {
modelValue?: number | string | null
@ -10,7 +10,7 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)!
const readOnly = inject(ReadonlyInj, false)
let isYearInvalid = $ref(false)
@ -53,7 +53,7 @@ watch(
{ flush: 'post' },
)
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : editEnabled.value ? 'Select year' : ''))
const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : ''))
</script>
<template>
@ -63,9 +63,9 @@ const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : editEnabled
:bordered="false"
class="!w-full px-1"
:placeholder="placeholder"
:allow-clear="!editEnabled"
:allow-clear="!readOnly"
:input-read-only="true"
:open="editEnabled ? false : open"
:open="readOnly ? false : open"
:dropdown-class-name="randomClass"
@click="open = !open"
@change="open = !open"

2
packages/nc-gui-v2/components/dashboard/GithubStarButton.vue

@ -3,7 +3,7 @@ import GithubButton from 'vue-github-button'
</script>
<template>
<GithubButton href="https://github.com/nocodb/nocodb" data-icon="octicon-star" :data-show-count="true" data-size="large"
<GithubButton href="https://github.com/nocodb/nocodb" data-icon="octicon-star" data-show-count="true" data-size="large"
>Star</GithubButton
>
</template>

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

@ -150,7 +150,7 @@ const activeTable = computed(() => {
<a-dropdown :trigger="['contextmenu']">
<div
class="p-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull"
:class="{ 'mb-[20px]': isSharedBase }"
style="direction: rtl"
>
@ -334,6 +334,7 @@ const activeTable = computed(() => {
.nc-tree-item.active {
@apply !text-primary font-weight-bold after:(!opacity-20);
@apply border-r-3 border-indigo-500;
svg {
@apply !text-primary;

50
packages/nc-gui-v2/components/shared-view/AskPassword.vue

@ -0,0 +1,50 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils'
interface Props {
modelValue: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
const { loadSharedView } = useSharedView()
const formState = ref({ password: undefined })
const vModel = useVModel(props, 'modelValue', emit)
const onFinish = async () => {
try {
await loadSharedView(route.params.viewId as string, formState.value.password)
vModel.value = false
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<a-modal
v-model:visible="vModel"
:closable="false"
width="28rem"
centered
:footer="null"
:mask-closable="false"
@close="vModel = false"
>
<div class="w-full flex flex-col">
<a-typography-title :level="4">This shared view is protected</a-typography-title>
<a-form ref="formRef" :model="formState" class="mt-2" @finish="onFinish">
<a-form-item name="password" :rules="[{ required: true, message: 'Password is required' }]">
<a-input-password v-model:value="formState.password" placeholder="Enter password" />
</a-form-item>
<a-button type="primary" html-type="submit">Unlock</a-button>
</a-form>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

87
packages/nc-gui-v2/components/shared-view/Form.vue

@ -0,0 +1,87 @@
<script setup lang="ts">
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { FieldsInj, MetaInj } from '#imports'
const fields = inject(FieldsInj, ref([]))
const meta = inject(MetaInj)
const { sharedView } = useSharedView()
const formState = ref(fields.value.reduce((a, v) => ({ ...a, [v.title]: undefined }), {}))
function isRequired(_columnObj: Record<string, any>, required = false) {
let columnObj = _columnObj
if (
columnObj.uidt === UITypes.LinkToAnotherRecord &&
columnObj.colOptions &&
columnObj.colOptions.type === RelationTypes.BELONGS_TO
) {
columnObj = fields.value.find((c: Record<string, any>) => c.id === columnObj.colOptions.fk_child_column_id) as Record<
string,
any
>
}
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
useSmartsheetStoreOrThrow()
useProvideSmartsheetRowStore(meta, formState)
const formRef = ref()
</script>
<template>
<div class="flex flex-col my-4 space-y-2 mx-32 items-center">
<div class="flex w-2/3 flex-col mt-10">
<div class="flex flex-col items-start px-14 py-8 bg-gray-50 rounded-md w-full">
<a-typography-title class="border-b-1 border-gray-100 w-full pb-3 nc-share-form-title" :level="1">
{{ sharedView.view.heading }}
</a-typography-title>
<a-typography class="pl-1 text-sm nc-share-form-desc">{{ sharedView.view.subheading }}</a-typography>
</div>
<a-form ref="formRef" :model="formState" class="mt-8 pb-12 mb-8 px-3 bg-gray-50 rounded-md">
<div v-for="(field, index) in fields" :key="index" class="flex flex-col mt-4 px-10 pt-6 space-y-2">
<div class="flex">
<SmartsheetHeaderVirtualCell
v-if="isVirtualCol(field)"
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
v-else
:column="{ ...field, title: field.label || field.title }"
:required="isRequired(field, field.required)"
:hide-menu="true"
/>
</div>
<a-form-item
v-if="isVirtualCol(field)"
class="ma-0 gap-0 pa-0"
:class="`nc-form-field-${field.title}`"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetVirtualCell v-model="formState[field.title]" class="nc-input" :column="field" />
</a-form-item>
<a-form-item
v-else
class="ma-0 gap-0 pa-0"
:class="`nc-form-field-${field.title}`"
:name="field.title"
:rules="[{ required: field.required, message: `${field.title} is required` }]"
>
<SmartsheetCell v-model="formState[field.title]" class="nc-input" :column="field" :edit-enabled="true" />
</a-form-item>
</div>
</a-form>
</div>
</div>
</template>
<style scoped lang="scss">
.nc-input {
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex align-center border-solid border-1 border-primary;
}
</style>

32
packages/nc-gui-v2/components/shared-view/Grid.vue

@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context'
const { sharedView, meta, columns } = useSharedView()
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, columns)
provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
</script>
<template>
<div class="nc-container flex flex-col h-full mt-4 px-6">
<SmartsheetToolbar />
<SmartsheetGrid class="px-3" />
</div>
</template>
<style scoped>
.nc-container {
height: calc(100% - var(--header-height));
flex: 1 1 100%;
}
</style>

2
packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue

@ -43,7 +43,7 @@ const aggrFunctionsList = [
]
if (!vModel.value.fk_relation_column_id) vModel.value.fk_relation_column_id = null
if (!vModel.value.fk_rollup_column_id) vModel.value.value.fk_rollup_column_id = null
if (!vModel.value.fk_rollup_column_id) vModel.value.fk_rollup_column_id = null
if (!vModel.value.rollup_function) vModel.value.rollup_function = null
const refTables = $computed(() => {

3
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -24,7 +24,7 @@ function onVisibleChange() {
</script>
<template>
<div class="flex items-center w-full text-xs text-normal">
<div class="flex items-center w-full text-xs text-normal text-gray-500 font-weight-medium" :class="{ 'h-full': column }">
<SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>
@ -36,6 +36,7 @@ function onVisibleChange() {
<a-dropdown
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"

4
packages/nc-gui-v2/components/smartsheet-header/Menu.vue

@ -53,8 +53,8 @@ const setAsPrimaryValue = async () => {
</script>
<template>
<a-dropdown placement="bottomRight" :trigger="['hover']">
<MdiMenuDownIcon class="text-grey nc-ui-dt-dropdown" />
<a-dropdown placement="bottomRight" :trigger="['click']">
<MdiMenuDownIcon class="h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0" />
<template #overlay>
<a-menu class="shadow bg-white">
<a-menu-item @click="emit('edit')">

4
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -101,7 +101,7 @@ function onVisibleChange() {
</script>
<template>
<div class="flex items-center w-full text-xs text-normal">
<div class="flex items-center w-full text-xs text-gray-500 font-weight-medium" :class="{ 'h-full': column }">
<SmartsheetHeaderVirtualCellIcon v-if="column" />
<a-tooltip placement="bottom">
@ -114,11 +114,13 @@ function onVisibleChange() {
<span v-if="column.rqd || required" class="text-red-500">&nbsp;*</span>
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>
<a-dropdown
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"

55
packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { toRef } from 'vue'
import { ColumnInj } from '~/context'
import GenericIcon from '~icons/mdi/square-rounded'
@ -16,35 +17,65 @@ import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before'
const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, ref(columnMeta))
const column = inject(ColumnInj, ref(columnMeta)) as Ref<ColumnType & { colOptions: LookupType }>
let relationColumn: ColumnType & { colOptions: LookupType }
if (column) {
const { isLookup, isBt, isRollup, isMm, isHm } = useVirtualCell(column as Ref<ColumnType>)
if (isLookup || isBt || isRollup || isMm || isHm) {
const meta = inject(MetaInj)
relationColumn = meta?.value.columns?.find((c) => c.id === column.value?.colOptions?.fk_relation_column_id) as ColumnType & {
colOptions: LinkToAnotherRecordType
}
}
}
const icon = computed(() => {
switch (column?.value?.uidt) {
case UITypes.LinkToAnotherRecord:
switch ((column?.value?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return MMIcon
return { icon: MMIcon, color: 'text-pink-500' }
case RelationTypes.HAS_MANY:
return HMIcon
return { icon: HMIcon, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO:
return BTIcon
return { icon: BTIcon, color: 'text-sky-500' }
}
break
case UITypes.SpecificDBType:
return SpecificDBTypeIcon
return { icon: SpecificDBTypeIcon, color: 'text-grey' }
case UITypes.Formula:
return FormulaIcon
return { icon: FormulaIcon, color: 'text-grey' }
case UITypes.Lookup:
return TableColumnPlusBefore
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return { icon: TableColumnPlusBefore, color: 'text-pink-500' }
case RelationTypes.HAS_MANY:
return { icon: TableColumnPlusBefore, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO:
return { icon: TableColumnPlusBefore, color: 'text-sky-500' }
}
return { icon: TableColumnPlusBefore, color: 'text-grey' }
case UITypes.Rollup:
return RollupIcon
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY:
return { icon: RollupIcon, color: 'text-pink-500' }
case RelationTypes.HAS_MANY:
return { icon: RollupIcon, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO:
return { icon: RollupIcon, color: 'text-sky-500' }
}
return { icon: RollupIcon, color: 'text-grey' }
case UITypes.Count:
return CountIcon
return { icon: CountIcon, color: 'text-grey' }
}
return GenericIcon
return { icon: GenericIcon, color: 'text-grey' }
})
</script>
<template>
<component :is="icon" class="text-grey mx-1 !text-sm" />
<component :is="icon.icon" class="mx-1 !text-sm" :class="icon.color" />
</template>

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

@ -8,13 +8,15 @@ import { comparisonOpList } from '~/utils/filterUtils'
import { ActiveViewInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import MdiDeleteIcon from '~icons/mdi/close-box'
import MdiAddIcon from '~icons/mdi/plus'
import type { Filter } from '~/lib/types'
const {
nested = false,
parentId,
autoSave = true,
hookId = null,
} = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean; hookId?: string }>()
modelValue,
} = defineProps<{ nested?: boolean; parentId?: string; autoSave: boolean; hookId?: string; modelValue?: Filter[] }>()
const emit = defineEmits(['update:filtersLength'])
@ -25,7 +27,6 @@ const activeView = inject(ActiveViewInj)
const reloadDataHook = inject(ReloadViewDataHookInj)
// todo: replace with inject or get from state
const shared = ref(false)
const { $e } = useNuxtApp()
@ -36,6 +37,7 @@ const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGr
() => {
reloadDataHook?.trigger()
},
modelValue,
)
const filterUpdateCondition = (filter: FilterType, i: number) => {
@ -152,7 +154,7 @@ defineExpose({
<span class="col-span-3" />
<div class="col-span-5">
<SmartsheetToolbarColumnFilter
v-if="filter.id || shared"
v-if="filter.id || filter.children"
ref="nestedFilters"
v-model="filter.children"
:parent-id="filter.id"

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

@ -1,12 +1,13 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import type ColumnFilter from './ColumnFilter.vue'
import { ActiveViewInj, IsLockedInj } from '~/context'
import { ActiveViewInj, IsLockedInj, IsPublicInj } from '~/context'
import MdiFilterIcon from '~icons/mdi/filter-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const isLocked = inject(IsLockedInj)
const activeView = inject(ActiveViewInj)
const isPublic = inject(IsPublicInj)
const { filterAutoSave } = useGlobal()
@ -50,7 +51,7 @@ const applyChanges = async () => {
:auto-save="filterAutoSave"
@update:filters-length="filtersLength = $event"
>
<div class="d-flex align-end mt-2 min-h-[30px]" @click.stop>
<div v-if="!isPublic" class="d-flex align-end mt-2 min-h-[30px]" @click.stop>
<a-checkbox id="col-filter-checkbox" v-model:checked="filterAutoSave" class="col-filter-checkbox" hide-details dense>
<span class="text-grey text-xs">
{{ $t('msg.info.filterAutoApply') }}

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

@ -21,7 +21,7 @@ const {
showAll,
hideAll,
saveOrUpdate,
} = useViewColumns(activeView, meta, false, () => reloadDataHook.trigger())
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
watch(
() => (activeView.value as any)?.id,

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

@ -6,6 +6,8 @@ import FileSaver from 'file-saver'
import { message } from 'ant-design-vue'
import {
ActiveViewInj,
FieldsInj,
IsPublicInj,
MetaInj,
extractSdkResponseErrorMsg,
inject,
@ -22,7 +24,7 @@ enum ExportTypes {
const sharedViewListDlg = ref(false)
const publicViewId = null
const isPublicView = inject(IsPublicInj, ref(false))
const isView = false
@ -33,7 +35,7 @@ const { project } = useProject()
const { $api } = useNuxtApp()
const meta = inject(MetaInj)
const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj)
const showWebhookDrawer = ref(false)
@ -46,31 +48,13 @@ const exportFile = async (exportType: ExportTypes) => {
let offset = 0
let c = 1
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob'
try {
while (!isNaN(offset) && offset > -1) {
let res
if (publicViewId) {
// TODO: pending for shared view
// const { data, headers } = await $api.public.csvExport(publicViewId, exportType, {
// format: responseType,
// query: {
// fields:
// queryParams && queryParams.fieldsOrder && queryParams.fieldsOrder.filter((c: number) => queryParams.showFields[c]),
// offset,
// sortArrJson: JSON.stringify(
// reqPayload &&
// reqPayload.sorts &&
// reqPayload.sorts.map(({ fk_column_id, direction }) => ({
// direction,
// fk_column_id,
// })),
// ),
// filterArrJson: JSON.stringify(reqPayload && reqPayload.filters),
// },
// headers: {
// 'xc-password': reqPayload && reqPayload.password,
// },
// } as Record<string, any>)
if (isPublicView.value) {
const { exportFile: sharedViewExportFile } = useSharedView()
res = await sharedViewExportFile(fields.value, offset, exportType, responseType)
} else {
res = await $api.dbViewRow.export(
'noco',
@ -137,7 +121,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div>
<div
v-if="isUIAllowed('csvImport') && !isView"
v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']"
class="nc-menu-item"
@click="quickImportDialog = true"
@ -148,7 +132,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div>
<div
v-if="isUIAllowed('SharedViewList') && !isView"
v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']"
class="nc-menu-item"
@click="sharedViewListDlg = true"
@ -159,7 +143,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div>
<div
v-if="isUIAllowed('webhook') && !isView"
v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']"
class="nc-menu-item"
@click="showWebhookDrawer = true"

5
packages/nc-gui-v2/components/smartsheet/Cell.vue

@ -54,6 +54,10 @@ const isManualSaved = $computed(() => {
return [UITypes.Currency, UITypes.Duration].includes(column?.value?.uidt as UITypes)
})
const isPrimary = computed(() => {
return column?.value?.pv
})
const vModel = computed({
get: () => props.modelValue,
set: (val) => {
@ -108,6 +112,7 @@ const syncAndNavigate = (dir: NavigateDir) => {
<template>
<div
class="nc-cell w-full h-full"
:class="{ 'text-blue-600': isPrimary }"
@keydown.stop.left
@keydown.stop.right
@keydown.stop.up

21
packages/nc-gui-v2/components/smartsheet/Form.vue

@ -42,17 +42,26 @@ const secondsRemain = ref(0)
const isEditable = isUIAllowed('editFormView' as Permission)
const meta = inject(MetaInj)
const meta = inject(MetaInj)!
const view = inject(ActiveViewInj)
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view as any)
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, false, async () => {
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, async () => {
await loadFormView()
setFormData()
})
const { syncLTARRefs } = useProvideSmartsheetRowStore(
meta,
ref({
row: formState,
oldRow: {},
rowMeta: { new: true },
}),
)
const columns = computed(() => meta?.value?.columns || [])
const localColumns = ref<Record<string, any>[]>([])
@ -92,7 +101,11 @@ async function submitForm() {
return
}
await insertRow(formState)
const insertedRowData = await insertRow(formState)
if (insertedRowData) {
await syncLTARRefs(insertedRowData)
}
submitted.value = true
}
@ -527,6 +540,7 @@ onMounted(async () => {
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
@click.stop.prevent
/>
</a-form-item>
@ -542,6 +556,7 @@ onMounted(async () => {
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"
:column="element"
:edit-enabled="true"
@click.stop.prevent
/>
</a-form-item>

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

@ -23,8 +23,10 @@ import {
IsFormInj,
IsGridInj,
IsLockedInj,
IsPublicInj,
MetaInj,
PaginationDataInj,
ReadonlyInj,
ReloadViewDataHookInj,
} from '~/context'
import { NavigateDir } from '~/lib'
@ -34,10 +36,12 @@ const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
const isPublicView = inject(IsPublicInj, ref(false))
// keep a root fields variable and will get modified from
// fields menu and get used in grid and gallery
const fields = inject(FieldsInj, ref([]))
const readonly = inject(ReadonlyInj, ref(false))
const isLocked = inject(IsLockedInj, false)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
@ -45,8 +49,6 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj)
const { isUIAllowed } = useUIPermission()
// todo: get from parent ( inject or use prop )
const isPublicView = false
const isView = false
const selected = reactive<{ row: number | null; col: number | null }>({ row: null, col: null })
@ -80,7 +82,6 @@ const {
} = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
onMounted(loadGridViewColumns)
provide(IsFormInj, ref(false))
@ -149,7 +150,7 @@ const clearCell = async (ctx: { row: number; col: number }) => {
const { copy } = useClipboard()
const makeEditable = (row: Row, col: ColumnType) => {
if (isPublicView || editEnabled || isView) {
if (isPublicView.value || editEnabled || isView) {
return
}
if (!isPkAvail.value && !row.rowMeta.new) {
@ -292,11 +293,15 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<div class="flex flex-col h-100 min-h-0 w-100">
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table ref="smartTable" class="xc-row-table nc-grid backgroundColorDefault" @contextmenu.prevent="contextMenu = true">
<table
ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto"
@contextmenu.prevent="contextMenu = true"
>
<thead>
<tr class="nc-grid-header border-1 bg-gray-100 sticky top[-1px]">
<th>
<div class="w-full h-full bg-gray-100 flex min-w-[80px] pl-5 pr-1 items-center">
<div class="w-full h-full bg-gray-100 flex min-w-[70px] pl-5 pr-1 items-center">
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
@ -319,14 +324,14 @@ const expandForm = (row: Row, state: Record<string, any>) => {
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readonly" />
<SmartsheetHeaderCell v-else :column="col" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="readonly" />
</div>
</th>
<!-- v-if="!isLocked && !isVirtual && !isPublicView && _isUIAllowed('add-column')" -->
<th
v-if="isUIAllowed('add-column')"
v-if="!readonly && isUIAllowed('add-column')"
v-t="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
@ -354,16 +359,19 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<template #default="{ state }">
<tr class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
<div class="align-center flex min-w-[80px]">
<div class="nc-row-no" :class="{ hidden: row.rowMeta.selected }">{{ rowIndex + 1 }}</div>
<div class="align-center flex gap-1 min-w-[55px]">
<div v-if="!readonly" class="nc-row-no text-xs text-gray-500" :class="{ hidden: row.rowMeta.selected }">
{{ rowIndex + 1 }}
</div>
<div
v-if="!readonly"
:class="{ hidden: !row.rowMeta.selected, flex: row.rowMeta.selected }"
class="nc-row-expand-and-checkbox"
>
<a-checkbox v-model:checked="row.rowMeta.selected" />
</div>
<span class="flex-1" />
<div class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<div v-if="!readonly" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<span
v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@ -410,7 +418,12 @@ const expandForm = (row: Row, state: Record<string, any>) => {
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
:edit-enabled="editEnabled && selected.col === colIndex && selected.row === rowIndex"
:edit-enabled="
isUIAllowed('xcDatatableEditable') &&
editEnabled &&
selected.col === colIndex &&
selected.row === rowIndex
"
:row-index="rowIndex"
:active="selected.col === colIndex && selected.row === rowIndex"
@update:edit-enabled="editEnabled = false"

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

@ -1,7 +1,9 @@
<script setup lang="ts">
import { useSmartsheetStoreOrThrow } from '~/composables'
import { IsPublicInj, useSmartsheetStoreOrThrow } from '#imports'
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
const { allowCSVDownload } = useSharedView()
const isPublic = inject(IsPublicInj, ref(false))
</script>
<template>
@ -12,11 +14,11 @@ const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
<SmartsheetToolbarSortListMenu v-if="isGrid || isGallery" />
<SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarShareView v-if="(isForm || isGrid) && !isPublic" />
<SmartsheetToolbarMoreActions v-if="isGrid" />
<SmartsheetToolbarMoreActions v-if="(isGrid && !isPublic) || (isGrid && isPublic && allowCSVDownload)" />
<div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid || isGallery" class="shrink mr-2" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2" />
</div>
</template>

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

@ -16,7 +16,7 @@ const BelongsTo = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.v
const Rollup = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const Formula = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const Formula = defineAsyncComponent(() => import('../virtual-cell/Formula.vue'))
const Count = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { enumColor, nextTick, ref, timeAgo, useExpandedFormStoreOrThrow, watch } from '#imports'
import { enumColor, ref, timeAgo, useExpandedFormStoreOrThrow, watch } from '#imports'
const { loadCommentsAndLogs, commentsAndLogs, isCommentsLoading, commentsOnly, saveComment, isYou, comment } =
useExpandedFormStoreOrThrow()
@ -13,52 +13,53 @@ const showborder = ref(false)
watch(
commentsAndLogs,
() => {
nextTick(() => {
// todo: replace setTimeout
setTimeout(() => {
if (commentsWrapperEl.value) commentsWrapperEl.value.scrollTop = commentsWrapperEl.value?.scrollHeight
})
}, 200)
},
{ immediate: true },
)
</script>
<template>
<div class="h-full d-flex flex-column w-full">
<div ref="commentsWrapperEl" class="flex-grow-1 min-h-[100px] overflow-y-auto scrollbar-thin-primary p-2">
<div class="h-full flex flex-col w-full bg-[#eceff1] p-2">
<div ref="commentsWrapperEl" class="flex-grow-1 min-h-[100px] overflow-y-auto scrollbar-thin-primary p-2 space-y-2">
<v-skeleton-loader v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" />
<template v-else>
<div v-for="log of commentsAndLogs" :key="log.id" class="flex gap-1 text-xs">
<MdiAccountCircle class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" />
<div class="flex-grow">
<p class="mb-1 caption edited-text text-[10px] text-gray">
<p class="mb-1 caption edited-text text-[10px] text-gray-500">
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }}
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }}
</p>
<p
v-if="log.op_type === 'COMMENT'"
class="caption mb-0 nc-chip w-full min-h-20px"
class="block caption my-2 nc-chip w-full min-h-20px p-2 rounded"
:style="{ backgroundColor: enumColor.light[2] }"
>
{{ log.description }}
</p>
<p v-else v-dompurify-html="log.details" class="caption mb-0" style="word-break: break-all" />
<p v-else v-dompurify-html="log.details" class="caption my-3" style="word-break: break-all" />
<p class="time text-right text-[10px] mb-0">
<p class="time text-right text-[10px] mb-0 mt-1 text-gray-500">
{{ timeAgo(log.created_at) }}
</p>
</div>
</div>
</template>
</div>
<div class="border-1 my-2 w-full ml-6" />
<div class="border-1 my-2 w-full" />
<div class="p-0">
<div class="flex justify-center">
<a-checkbox v-model:checked="commentsOnly" @change="loadCommentsAndLogs"
><span class="text-[11px] text-gray-500">Comments only</span>
</a-checkbox>
</div>
<div class="flex-shrink-1 mt-2 d-flex pl-4">
<div class="flex-shrink-1 mt-2 d-flex">
<a-input
v-model:value="comment"
class="!text-xs"

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

@ -102,7 +102,7 @@ export default {
<template>
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" />
<a-card class="!bg-gray-100">
<div class="!bg-gray-100 rounded">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[70vh]">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">
@ -131,7 +131,7 @@ export default {
</div>
</div>
</div>
</a-card>
</div>
</a-modal>
</template>
@ -154,7 +154,7 @@ export default {
}
.nc-form-wrapper {
max-height: max(calc(90vh - 100px), 600px);
max-height: max(calc(90vh - 150px), 600px);
height: max-content !important;
}
</style>

7
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -5,6 +5,7 @@ import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue'
import {
ActiveViewInj,
IsFormInj,
MetaInj,
ViewListInj,
computed,
@ -22,6 +23,8 @@ const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj, ref())
const isForm = inject(IsFormInj)
const { views, loadViews } = useViews(meta)
const { isUIAllowed } = useUIPermission()
@ -122,7 +125,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
</Transition>
</a-tooltip>
<Toolbar v-if="isOpen" class="flex items-center py-3 px-3 justify-between border-b-1" />
<Toolbar v-if="isOpen" :class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }" />
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">
<template #start>
@ -144,7 +147,7 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
</div>
</a-tooltip>
<div class="dot" />
<div v-if="!isForm" class="dot" />
</template>
</Toolbar>

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

@ -7,7 +7,7 @@ const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('activity.addRow') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group">
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group nc-sidebar-add-row">
<MdiPlusOutline class="cursor-pointer group-hover:(!text-white)" @click="emits('addRow')" />
</div>
</a-tooltip>

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

@ -49,7 +49,7 @@ const Icon = computed(() => {
<template>
<a-dropdown max-width="350" :trigger="['click']">
<div class="nc-sidebar-right-item hover:after:bg-indigo-500 group">
<div class="nc-sidebar-right-item hover:after:bg-indigo-500 group nc-sidebar-lock-menu">
<Icon class="cursor-pointer group-hover:(!text-white)" />
</div>

13
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue

@ -5,9 +5,12 @@ import Reload from './Reload.vue'
import ExportCache from './ExportCache.vue'
import DeleteCache from './DeleteCache.vue'
import DebugMeta from './DebugMeta.vue'
import { IsFormInj } from '#imports'
const { isUIAllowed } = useUIPermission()
const isForm = inject(IsFormInj)
const debug = $ref(false)
const clickCount = $ref(0)
@ -15,6 +18,7 @@ const clickCount = $ref(0)
<template>
<div
v-if="!isForm"
class="flex gap-2"
@click="
() => {
@ -39,18 +43,21 @@ const clickCount = $ref(0)
<div class="dot" />
</template>
<LockMenu v-if="isUIAllowed('view-type')" />
<LockMenu v-if="isUIAllowed('view-type')" @click.stop />
<div v-if="isUIAllowed('view-type')" class="dot" />
<Reload />
<Reload @click.stop />
<div class="dot" />
<AddRow v-if="isUIAllowed('xcDatatableEditable')" />
<AddRow v-if="isUIAllowed('xcDatatableEditable')" @click.stop />
<slot name="end" />
</div>
<div v-else>
<slot name="start" />
</div>
</template>
<style scoped>

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

@ -17,6 +17,7 @@ import {
watch,
watchEffect,
} from '#imports'
import type { TabItem } from '~/composables'
const { getMeta, metas } = useMetas()
@ -40,6 +41,11 @@ watchEffect(async () => {
const reloadEventHook = createEventHook<void>()
const { isGallery, isGrid, isForm } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
// provide the sidebar injection state
provideSidebar({ storageKey: 'nc-right-sidebar' })
// todo: move to store
provide(MetaInj, meta)
provide(TabMetaInj, tabMeta)
@ -47,11 +53,7 @@ provide(ActiveViewInj, activeView)
provide(IsLockedInj, false)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(FieldsInj, fields)
// provide the sidebar injection state
provideSidebar({ storageKey: 'nc-right-sidebar' })
const { isGallery, isGrid, isForm } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
provide(IsFormInj, isForm)
watch(tabMeta, async (newTabMeta, oldTabMeta) => {
if (newTabMeta !== oldTabMeta && newTabMeta?.id) await getMeta(newTabMeta.id)

14
packages/nc-gui-v2/components/tabs/auth/UserManagement.vue

@ -192,7 +192,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="text-gray-500">Reload</div>
</div>
</a-button>
<a-button v-if="isUIAllowed('newUser')" size="middle" type="primary" ghost @click="onInvite">
<a-button v-if="isUIAllowed('newUser')" size="middle" type="primary" ghost class="nc-invite-team" @click="onInvite">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiAccountPlusOutline class="mr-1" />
<div>{{ $t('activity.inviteTeam') }}</div>
@ -217,13 +217,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div class="flex w-4/6 flex-wrap">
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2 nc-user-row">
<div class="flex w-4/6 flex-wrap nc-user-email">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
<div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles] }">
<div class="rounded-full px-2 py-1 nc-user-role" :style="{ backgroundColor: projectRoleTagColors[user.roles] }">
{{ user.roles }}
</div>
</div>
@ -232,7 +232,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title>
<span>{{ $t('activity.editUser') }}</span>
</template>
<a-button type="text" class="!rounded-md" @click="onEdit(user)">
<a-button type="text" class="!rounded-md nc-user-edit" @click="onEdit(user)">
<template #icon>
<IcRoundEdit class="flex mx-auto h-[1rem] text-gray-500" />
</template>
@ -242,7 +242,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title>
<span>Add user to the project</span>
</template>
<a-button type="text" class="!rounded-md" @click="inviteUser(user)">
<a-button type="text" class="!rounded-md nc-user-invite" @click="inviteUser(user)">
<template #icon>
<MdiPlus class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>
@ -253,7 +253,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<template #title>
<span>Remove user from the project</span>
</template>
<a-button type="text" class="!rounded-md" @click="onDelete(user)">
<a-button type="text" class="!rounded-md nc-user-delete" @click="onDelete(user)">
<template #icon>
<MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>

4
packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue

@ -205,8 +205,8 @@ const clickInviteMore = () => {
<div class="flex flex-col w-1/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role">
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role">
<a-select v-model:value="usersData.role" class="nc-user-roles">
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option">
<div class="flex flex-row h-full justify-start items-center">
<div
class="px-2 py-1 flex rounded-full text-xs"

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

@ -5,7 +5,7 @@ import {
ActiveCellInj,
CellValueInj,
ColumnInj,
EditModeInj,
ReadonlyInj,
ReloadViewDataHookInj,
RowInj,
defineAsyncComponent,
@ -31,7 +31,7 @@ const row = inject(RowInj)!
const active = inject(ActiveCellInj)!
const editEnabled = inject(EditModeInj)
const readonly = inject(ReadonlyInj, false)
const listItemsDlg = ref(false)
@ -72,10 +72,10 @@ const unlinkRef = async (rec: Record<string, any>) => {
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template>
</div>
<div v-if="editEnabled" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<div v-if="!readonly" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<component
:is="addIcon"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500)"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500) nc-plus"
@click="listItemsDlg = true"
/>
</div>

18
packages/nc-gui-v2/components/virtual-cell/HasMany.vue

@ -4,8 +4,8 @@ import type { Ref } from 'vue'
import {
CellValueInj,
ColumnInj,
EditModeInj,
IsFormInj,
ReadonlyInj,
ReloadViewDataHookInj,
RowInj,
computed,
@ -32,7 +32,7 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const editEnabled = inject(EditModeInj)
const readonly = inject(ReadonlyInj, false)
const listItemsDlg = ref(false)
@ -49,9 +49,9 @@ await loadRelatedTableMeta()
const localCellValue = computed(() => {
if (cellValue?.value) {
return cellValue?.value
return cellValue?.value ?? []
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
return state?.value?.[column?.value.title as string] ?? []
}
return []
})
@ -83,19 +83,19 @@ const unlinkRef = async (rec: Record<string, any>) => {
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true"
>more...
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">
more...
</span>
</template>
</div>
<div class="flex-grow flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true"
/>
<MdiPlus
v-if="editEnabled"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
v-if="!readonly"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click="listItemsDlg = true"
/>
</div>

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

@ -4,8 +4,8 @@ import type { Ref } from 'vue'
import {
CellValueInj,
ColumnInj,
EditModeInj,
IsFormInj,
ReadonlyInj,
ReloadViewDataHookInj,
RowInj,
computed,
@ -31,7 +31,7 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj)
const editEnabled = inject(EditModeInj)
const readonly = inject(ReadonlyInj, false)
const listItemsDlg = ref(false)
@ -49,9 +49,9 @@ await loadRelatedTableMeta()
const localCellValue = computed(() => {
if (cellValue?.value) {
return cellValue?.value
return cellValue?.value ?? []
} else if (isNew.value) {
return state?.value?.[column?.value.title as string]
return state?.value?.[column?.value.title as string] ?? []
}
return []
})
@ -89,11 +89,11 @@ const unlinkRef = async (rec: Record<string, any>) => {
</div>
<div class="flex-1 flex justify-end gap-1 min-h-[30px] align-center">
<MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500" @click="childListDlg = true" />
<MdiArrowExpand class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" @click="childListDlg = true" />
<MdiPlus
v-if="editEnabled"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500"
v-if="!readonly"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click="listItemsDlg = true"
/>
</div>

10
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { ActiveCellInj, EditModeInj, IsFormInj, defineAsyncComponent, inject, ref, useLTARStoreOrThrow } from '#imports'
import { ActiveCellInj, IsFormInj, ReadonlyInj, defineAsyncComponent, inject, ref, useLTARStoreOrThrow } from '#imports'
interface Props {
value?: string | number | boolean
@ -14,7 +14,7 @@ const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/ex
const { relatedTableMeta } = useLTARStoreOrThrow()!
const editEnabled = inject(EditModeInj)!
const readonly = inject(ReadonlyInj, false)
const active = inject(ActiveCellInj, ref(false))
@ -31,19 +31,19 @@ export default {
<template>
<div
class="group py-1 px-2 flex align-center gap-1 bg-gray-200/50 hover:bg-gray-200 rounded-[20px]"
class="group py-1 px-2 mr-1 my-1 flex align-center bg-blue-100/60 hover:bg-blue-100/40 rounded-[2px]"
:class="{ active }"
@click="expandedFormDlg = true"
>
<span class="name">{{ value }}</span>
<div v-show="active || isForm" v-if="editEnabled" class="flex align-center">
<div v-show="active || isForm" v-if="!readonly" class="flex align-center">
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div>
<Suspense>
<ExpandedForm
v-if="editEnabled"
v-if="!readonly && expandedFormDlg"
v-model="expandedFormDlg"
:row="{ row: item }"
:meta="relatedTableMeta"

32
packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue

@ -3,8 +3,8 @@ import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import {
ColumnInj,
EditModeInj,
IsFormInj,
ReadonlyInj,
computed,
useLTARStoreOrThrow,
useSmartsheetRowStoreOrThrow,
@ -24,7 +24,7 @@ const isForm = inject(IsFormInj, ref(false))
const column = inject(ColumnInj)
const editEnabled = inject(EditModeInj)
const readonly = inject(ReadonlyInj, false)
const {
childrenList,
@ -43,7 +43,7 @@ const { isNew, state, removeLTARRef } = useSmartsheetRowStoreOrThrow()
watch(
[vModel, isForm],
(nextVal) => {
if (nextVal[0] || nextVal[1]) {
if ((nextVal[0] || nextVal[1]) && !isNew.value) {
loadChildrenList()
}
},
@ -59,6 +59,12 @@ const unlinkRow = async (row: Record<string, any>) => {
}
}
const unlinkIfNewRow = async (row: Record<string, any>) => {
if (isNew.value) {
removeLTARRef(row, column?.value as ColumnType)
}
}
const container = computed(() =>
isForm?.value
? h('div', {
@ -79,7 +85,7 @@ const expandedFormRow = ref()
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button v-if="editEnabled" type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')">
<a-button v-if="!readonly" type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1">
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
Link to '{{ meta.title }}'
@ -101,18 +107,19 @@ const expandedFormRow = ref()
>
<div class="flex align-center">
<div class="flex-grow overflow-hidden min-w-0">
{{ row[relatedTablePrimaryValueProp]
}}<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
{{ row[relatedTablePrimaryValueProp] }}
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div>
<div class="flex-1"></div>
<div v-if="editEnabled" class="flex gap-2">
<div v-if="!readonly" class="flex gap-2">
<MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="unlinkRow(row)"
/>
<MdiDeleteOutline
v-if="!readonly"
class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@click.stop="deleteRelatedRow(row)"
@click.stop="deleteRelatedRow(row, unlinkIfNewRow)"
/>
</div>
</div>
@ -128,12 +135,17 @@ const expandedFormRow = ref()
show-less-items
/>
</template>
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-empty
v-else
:class="{ 'my-10': !isForm, 'my-1 !text-xs': isForm }"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
:image-style="isForm ? { height: '20px' } : {}"
/>
</div>
<Suspense>
<ExpandedForm
v-if="expandedFormRow"
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="{ row: expandedFormRow }"
:meta="relatedTableMeta"

10
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -47,8 +47,12 @@ const linkRow = async (row: Record<string, any>) => {
vModel.value = false
}
watch(vModel, () => {
if (vModel.value) {
/** reload list on modal open */
watch(vModel, (nextVal, prevVal) => {
if (nextVal && !prevVal) {
/** reset query and limit */
childrenExcludedListPagination.query = ''
childrenExcludedListPagination.page = 1
loadChildrenExcludedList()
}
})
@ -100,7 +104,7 @@ const newRowState = computed(() => {
size="small"
></a-input>
<div class="flex-1" />
<MdiReload class="cursor-pointer text-gray-500" @click="loadChildrenExcludedList" />
<MdiReload class="cursor-pointer text-gray-500 nc-reload" @click="loadChildrenExcludedList" />
<a-button type="primary" size="small" @click="expandedFormDlg = true">Add new record</a-button>
</div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows">

8
packages/nc-gui-v2/composables/useGridViewColumnWidth.ts

@ -3,6 +3,7 @@ import type { ColumnType, GridColumnType, GridType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useMetas } from './useMetas'
import { useUIPermission } from './useUIPermission'
import { IsPublicInj } from '~/context'
export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) | undefined>) {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
@ -13,6 +14,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
const gridViewCols = ref<Record<string, GridColumnType>>({})
const resizingCol = ref('')
const resizingColWidth = ref('200px')
const isPublic = inject(IsPublicInj, ref(false))
const columns = computed<ColumnType[]>(() => metas?.value?.[(view?.value as any)?.fk_model_id as string]?.columns)
@ -35,8 +37,8 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
)
const loadGridViewColumns = async () => {
if (!view.value?.id) return
const colsData: GridColumnType[] = await $api.dbView.gridColumnsList(view.value.id)
if (!view.value?.id && !isPublic.value) return
const colsData: GridColumnType[] = isPublic.value ? columns.value : await $api.dbView.gridColumnsList(view.value.id)
gridViewCols.value = colsData.reduce<Record<string, GridColumnType>>(
(o, col) => ({
...o,
@ -56,7 +58,7 @@ export function useGridViewColumnWidth(view: Ref<(GridType & { id?: string }) |
}
// sync with server if allowed
if (isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) {
if (!isPublic.value && isUIAllowed('gridColUpdate') && gridViewCols.value[id]?.id) {
$api.dbView.gridColumnUpdate(gridViewCols.value[id].id as string, {
width,
})

9
packages/nc-gui-v2/composables/useLTARStore.ts

@ -139,7 +139,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
}
}
const deleteRelatedRow = async (row: Record<string, any>) => {
const deleteRelatedRow = async (row: Record<string, any>, onSuccess?: (row: Record<string, any>) => void) => {
Modal.confirm({
title: 'Do you want to delete the record?',
type: 'warning',
@ -148,7 +148,12 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
try {
$api.dbTableRow.delete(NOCO, project.value.id as string, relatedTableMeta.value.id as string, id as string)
reloadData?.()
await loadChildrenList()
/** reload child list if not a new row */
if (!isNewRow?.value) {
await loadChildrenList()
}
onSuccess?.(row)
} catch (e: any) {
message.error(`Delete failed: ${await extractSdkResponseErrorMsg(e)}`)
}

10
packages/nc-gui-v2/composables/useMetas.ts

@ -12,6 +12,14 @@ export function useMetas() {
const metas = useState<{ [idOrTitle: string]: TableType | any }>('metas', () => ({}))
const loadingState = useState<Record<string, boolean>>('metas-loading-state', () => ({}))
const setMeta = async (model: any) => {
metas.value = {
...metas.value,
[model.id!]: model,
[model.title]: model,
}
}
const getMeta = async (tableIdOrTitle: string, force = false): Promise<TableType | TableInfoType | null> => {
if (!tableIdOrTitle) return null
/** wait until loading is finished if requesting same meta */
@ -83,5 +91,5 @@ export function useMetas() {
}
}
return { getMeta, clearAllMeta, metas, removeMeta }
return { getMeta, clearAllMeta, metas, removeMeta, setMeta }
}

24
packages/nc-gui-v2/composables/useProject.ts

@ -2,6 +2,7 @@ import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useRoute, useState } from '#app'
import type { ProjectMetaInfo } from '~/lib'
import { USER_PROJECT_ROLES } from '~/lib'
export function useProject(projectId?: MaybeRef<string>) {
@ -13,10 +14,17 @@ export function useProject(projectId?: MaybeRef<string>) {
const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
const { includeM2M } = useGlobal()
const projectMetaInfo = useState<ProjectMetaInfo | undefined>('projectMetaInfo')
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) {
const data = await $api.project.metaGet(project.value.id!, {}, {})
projectMetaInfo.value = data
}
}
async function loadProjectRoles() {
projectRoles.value = {}
@ -59,5 +67,17 @@ export function useProject(projectId?: MaybeRef<string>) {
)
const isSharedBase = computed(() => projectType === 'base')
return { project, tables, loadProjectRoles, loadProject, loadTables, isMysql, isPg, sqlUi, isSharedBase }
return {
project,
tables,
loadProjectRoles,
loadProject,
loadTables,
isMysql,
isPg,
sqlUi,
isSharedBase,
loadProjectMetaInfo,
projectMetaInfo,
}
}

108
packages/nc-gui-v2/composables/useSharedView.ts

@ -0,0 +1,108 @@
import type { ColumnType, ExportTypes, FilterType, PaginatedType, SortType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
export function useSharedView() {
const nestedFilters = useState<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>(
'nestedFilters',
() => [],
)
const paginationData = useState<PaginatedType>('paginationData', () => ({ page: 1, pageSize: 25 }))
const sharedView = useState<ViewType>('sharedView')
const sorts = useState<SortType[]>('sorts', () => [])
const password = useState<string | undefined>('password')
const allowCSVDownload = useState<boolean>('allowCSVDownload', () => false)
const meta = ref<TableType>(sharedView.value?.model)
const columns = ref<ColumnType[]>(sharedView.value?.model?.columns)
const formColumns = computed(
() =>
columns.value
.filter(
(f: Record<string, any>) =>
f.show && f.uidt !== UITypes.Rollup && f.uidt !== UITypes.Lookup && f.uidt !== UITypes.Formula,
)
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order)
.map((c: Record<string, any>) => ({ ...c, required: !!(c.required || 0) })) ?? [],
)
const { $api } = useNuxtApp()
const { setMeta } = useMetas()
const loadSharedView = async (viewId: string, localPassword: string | undefined = undefined) => {
const viewMeta = await $api.public.sharedViewMetaGet(viewId, {
headers: {
'xc-password': localPassword ?? password.value,
},
})
allowCSVDownload.value = JSON.parse(viewMeta.meta).allowCSVDownload
if (localPassword) password.value = localPassword
sharedView.value = viewMeta
meta.value = viewMeta.model
columns.value = viewMeta.model.columns
setMeta(viewMeta.model)
const relatedMetas = { ...viewMeta.relatedMetas }
Object.keys(relatedMetas).forEach((key) => setMeta(relatedMetas[key]))
}
const fetchSharedViewData = async () => {
const page = paginationData.value.page || 1
const pageSize = paginationData.value.pageSize || 25
const { data } = await $api.public.dataList(
sharedView?.value?.uuid,
{
offset: (page - 1) * pageSize,
filterArrJson: JSON.stringify(nestedFilters.value),
sortArrJson: JSON.stringify(sorts.value),
} as any,
{
headers: {
'xc-password': password.value,
},
},
)
return data
}
const exportFile = async (
fields: any[],
offset: number,
type: ExportTypes.EXCEL | ExportTypes.CSV,
responseType: 'base64' | 'blob',
) => {
return await $api.public.csvExport(sharedView.value?.uuid, type, {
format: responseType as any,
query: {
fields: fields.map((field) => field.title),
offset,
sortArrJson: JSON.stringify(sorts.value),
filterArrJson: JSON.stringify(nestedFilters.value),
},
headers: {
'xc-password': password.value,
},
})
}
return {
sharedView,
loadSharedView,
meta,
columns,
nestedFilters,
fetchSharedViewData,
paginationData,
sorts,
exportFile,
formColumns,
allowCSVDownload,
}
}

7
packages/nc-gui-v2/composables/useSmartsheetRowStore.ts

@ -5,7 +5,7 @@ import type { Ref } from 'vue'
import type { Row } from './useViewData'
import { useInjectionState, useMetas, useNuxtApp, useProject, useVirtualCell } from '#imports'
import { NOCO } from '~/lib'
import { extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
import { deepCompare, extractPkFromRow, extractSdkResponseErrorMsg } from '~/utils'
const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState((meta: Ref<TableType>, row: Ref<Row>) => {
const { $api } = useNuxtApp()
@ -23,6 +23,11 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
const { isHm, isMm, isBt } = $(useVirtualCell(ref(column)))
if (isHm || isMm) {
state.value[column.title!] = state.value[column.title!] || []
if (state.value[column.title!]!.find((ln: Record<string, any>) => deepCompare(ln, value))) {
return message.info('This value is already in the list')
}
state.value[column.title!]!.push(value)
} else if (isBt) {
state.value[column.title!] = value

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

@ -3,13 +3,10 @@ import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { watch } from 'vue'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp } from '#app'
import { IsPublicInj } from '#imports'
export function useViewColumns(
view: Ref<ViewType> | undefined,
meta: ComputedRef<TableType>,
isPublic = false,
reloadData?: () => void,
) {
export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRef<TableType>, reloadData?: () => void) {
const isPublic = inject(IsPublicInj, ref(false))
const fields = ref<
{
order: number
@ -31,7 +28,7 @@ export function useViewColumns(
let order = 1
if (view.value?.id) {
const data = (await $api.dbViewColumn.list(view.value.id)) as any[]
const data = (isPublic.value ? meta.value?.columns : await $api.dbViewColumn.list(view.value.id)) as any[]
const fieldById = data.reduce<Record<string, any>>((acc, curr) => {
curr.show = !!curr.show
@ -54,12 +51,19 @@ export function useViewColumns(
}
})
.sort((a, b) => a.order - b.order)
} else if (isPublic) {
fields.value = meta.value.columns as any
}
}
const showAll = async (ignoreIds?: any) => {
if (isPublic.value) {
fields.value = fields.value?.map((field) => ({
...field,
show: true,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.showAllColumn(view.value.id, {
@ -74,6 +78,14 @@ export function useViewColumns(
reloadData?.()
}
const hideAll = async (ignoreIds?: any) => {
if (isPublic.value) {
fields.value = fields.value?.map((field) => ({
...field,
show: false,
}))
reloadData?.()
return
}
if (view?.value?.id) {
if (ignoreIds) {
await $api.dbView.hideAllColumn(view.value.id, {
@ -89,6 +101,22 @@ export function useViewColumns(
}
const saveOrUpdate = async (field: any, index: number) => {
if (isPublic && fields.value) {
fields.value[index] = field
meta.value.columns = meta.value?.columns?.map((column) => {
if (column.id === field.fk_column_id) {
return {
...column,
...field,
}
}
return column
})
reloadData?.()
return
}
if (isUIAllowed('fieldsSync')) {
if (field.id && view?.value?.id) {
await $api.dbViewColumn.update(view.value.id, field.id, field)
@ -118,11 +146,13 @@ export function useViewColumns(
},
set(v: boolean) {
if (view?.value?.id) {
$api.dbView
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => reloadData?.())
if (!isPublic.value) {
$api.dbView
.update(view.value.id, {
show_system_fields: v,
})
.finally(() => reloadData?.())
}
;(view.value as any).show_system_fields = v
}
},

38
packages/nc-gui-v2/composables/useViewData.ts

@ -1,7 +1,8 @@
import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { message } from 'ant-design-vue'
import { NOCO, computed, extractPkFromRow, extractSdkResponseErrorMsg, ref, useNuxtApp, useProject } from '#imports'
import { useNuxtApp } from '#app'
import { IsPublicInj, NOCO, extractPkFromRow, extractSdkResponseErrorMsg, useProject } from '#imports'
const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({
@ -29,17 +30,30 @@ export function useViewData(
throw new Error('Table meta is not available')
}
const formattedData = ref<Row[]>([])
const paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const _paginationData = ref<PaginatedType>({ page: 1, pageSize: 25 })
const aggCommentCount = ref<{ row_id: string; count: number }[]>([])
const galleryData = ref<GalleryType>()
const formColumnData = ref<FormType>()
// todo: missing properties on FormType (success_msg, show_blank_form,
const formViewData = ref<FormType & { success_msg?: string; show_blank_form?: boolean }>()
const formattedData = ref<Row[]>([])
const isPublic = inject(IsPublicInj, ref(false))
const { project } = useProject()
const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
const { $api } = useNuxtApp()
const paginationData = computed({
get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
set: (value) => {
if (isPublic.value) {
sharedPaginationData.value = value
} else {
_paginationData.value = value
}
},
})
const selectedAllRecords = computed({
get() {
return !!formattedData.value.length && formattedData.value.every((row: Row) => row.rowMeta.selected)
@ -68,9 +82,7 @@ export function useViewData(
/** load row comments count */
const loadAggCommentsCount = async () => {
// todo: handle in public api
// if (this.isPublicView) {
// return;
// }
if (isPublic.value) return
const ids = formattedData.value
?.filter(({ rowMeta: { new: isNew } }) => !isNew)
@ -92,11 +104,14 @@ export function useViewData(
}
const loadData = async (params: Parameters<Api<any>['dbViewRow']['list']>[4] = {}) => {
if (!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) return
const response = await $api.dbViewRow.list('noco', project.value.id, meta.value.id, viewMeta.value.id, {
...params,
where: where?.value,
})
if ((!project?.value?.id || !meta?.value?.id || !viewMeta?.value?.id) && !isPublic.value) return
const response = !isPublic.value
? await $api.dbViewRow.list('noco', project.value.id!, meta.value.id!, viewMeta!.value.id, {
...params,
where: where?.value,
})
: await fetchSharedViewData()
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
@ -133,6 +148,7 @@ export function useViewData(
})
await syncCount()
return insertedData
} catch (error: any) {
message.error(await extractSdkResponseErrorMsg(error))
}

73
packages/nc-gui-v2/composables/useViewFilters.ts

@ -1,22 +1,53 @@
import type { FilterType, ViewType } from 'nocodb-sdk'
import type { ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { useNuxtApp, useUIPermission } from '#imports'
import { useMetas } from '~/composables/useMetas'
import { IsPublicInj, ReloadViewDataHookInj, useMetas, useNuxtApp, useUIPermission } from '#imports'
import type { Filter } from '~/lib'
export function useViewFilters(
view: Ref<ViewType> | undefined,
parentId?: string,
autoApply?: ComputedRef<boolean>,
reloadData?: () => void,
shared = false,
siblingFilters?: Filter[],
) {
const filters = ref<(FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string })[]>([])
const { nestedFilters } = useSharedView()
const reloadHook = inject(ReloadViewDataHookInj)
const _filters = ref<Filter[]>([])
const isPublic = inject(IsPublicInj, ref(false))
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const { metas } = useMetas()
const filters = computed({
get: () => (isPublic.value ? siblingFilters || nestedFilters.value : _filters.value),
set: (value) => {
if (isPublic.value) {
if (siblingFilters) {
siblingFilters = value
} else {
nestedFilters.value = value
}
nestedFilters.value = [...nestedFilters.value]
reloadHook?.trigger()
} else {
_filters.value = value
}
},
})
const placeholderFilter: Filter = {
comparison_op: 'eq',
value: '',
status: 'create',
logical_op: 'and',
}
const loadFilters = async (hookId?: string) => {
if (isPublic.value) return
if (hookId) {
if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
@ -55,12 +86,13 @@ export function useViewFilters(
}
}
}
reloadData?.()
}
const deleteFilter = async (filter: FilterType & { status: string }, i: number) => {
const deleteFilter = async (filter: Filter, i: number) => {
// if shared or sync permission not allowed simply remove it from array
if (shared || !isUIAllowed('filterSync')) {
if (isPublic.value || !isUIAllowed('filterSync')) {
filters.value.splice(i, 1)
reloadData?.()
} else {
@ -81,9 +113,14 @@ export function useViewFilters(
}
}
const saveOrUpdate = async (filter: FilterType & { status?: string }, i: number, force = false) => {
const saveOrUpdate = async (filter: Filter, i: number, force = false) => {
if (isPublic.value) {
filters.value[i] = { ...filter } as any
filters.value = [...filters.value]
return
}
if (!view?.value) return
if (shared || !isUIAllowed('filterSync')) {
if (!isUIAllowed('filterSync')) {
// skip
} else if (!autoApply?.value && !force) {
filter.status = filter.id ? 'update' : 'create'
@ -103,21 +140,19 @@ export function useViewFilters(
}
const addFilter = () => {
filters.value.push({
comparison_op: 'eq',
value: '',
status: 'create',
logical_op: 'and',
})
filters.value.push(placeholderFilter)
}
const addFilterGroup = async (parentId?: string) => {
filters.value.push({
parentId,
const addFilterGroup = async () => {
const child = placeholderFilter
const placeHolderGroupFilter: Filter = {
is_group: true,
status: 'create',
logical_op: 'and',
})
}
if (isPublic.value) placeHolderGroupFilter.children = [child]
filters.value.push(placeHolderGroupFilter)
const index = filters.value.length - 1
await saveOrUpdate(filters.value[index], index, true)
}

47
packages/nc-gui-v2/composables/useViewSorts.ts

@ -1,18 +1,39 @@
import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { useNuxtApp } from '#imports'
import { IsPublicInj, ReloadViewDataHookInj, useNuxtApp } from '#imports'
export function useViewSorts(
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
reloadData?: () => void,
) {
const sorts = ref<SortType[]>([])
const _sorts = ref<SortType[]>([])
const { sorts: sharedViewSorts, sharedView } = useSharedView()
const reloadHook = inject(ReloadViewDataHookInj)
const isPublic = inject(IsPublicInj, ref(false))
const sorts = computed<SortType[]>({
get: () => (isPublic.value ? sharedViewSorts.value : _sorts.value),
set: (value) => {
if (isPublic.value) {
sharedViewSorts.value = value
} else {
_sorts.value = value
}
reloadHook?.trigger()
},
})
const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const loadSorts = async () => {
if (isPublic.value) {
const sharedSorts = sharedView.value?.sorts || []
sorts.value = [...sharedSorts]
return
}
if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list
}
@ -20,6 +41,12 @@ export function useViewSorts(
const saveOrUpdate = async (sort: SortType, i: number) => {
// TODO:
// if (!this.shared && this._isUIAllowed('sortSync')) {
if (isPublic.value) {
sorts.value[i] = sort
sorts.value = [...sorts.value]
return
}
if (isUIAllowed('sortSync')) {
if (sort.id) {
await $api.dbTableSort.update(sort.id, sort)
@ -30,19 +57,23 @@ export function useViewSorts(
reloadData?.()
}
const addSort = () => {
sorts.value.push({
direction: 'asc',
})
sorts.value = [
...sorts.value,
{
direction: 'asc',
},
]
}
const deleteSort = async (sort: SortType, i: number) => {
// TOOD:
// if (!this.shared && sort.id && this._isUIAllowed('sortSync')) {
if (isUIAllowed('sortSync') && sort.id) {
if (isUIAllowed('sortSync') && sort.id && !isPublic.value) {
await $api.dbTableSort.delete(sort.id)
} else {
sorts.value.splice(i, 1)
}
sorts.value.splice(i, 1)
sorts.value = [...sorts.value]
}
return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate }
}

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

@ -6,6 +6,7 @@ import type { Row } from '~/composables'
import type { TabItem } from '~/composables/useTabs'
export const ActiveCellInj: InjectionKey<Ref<boolean>> = Symbol('active-cell')
export const IsPublicInj: InjectionKey<Ref<boolean>> = Symbol('is-public')
export const RowInj: InjectionKey<Ref<Row>> = Symbol('row')
export const ColumnInj: InjectionKey<Ref<ColumnType & { meta: any }>> = Symbol('column-injection')
export const MetaInj: InjectionKey<ComputedRef<TableType>> = Symbol('meta-injection')
@ -18,7 +19,7 @@ export const IsGridInj: InjectionKey<boolean> = Symbol('is-grid-injection')
export const IsLockedInj: InjectionKey<boolean> = Symbol('is-locked-injection')
export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection')
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<any> = Symbol('readonly-injection')
export const ReadonlyInj: InjectionKey<boolean> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')

37
packages/nc-gui-v2/layouts/shared-view.vue

@ -0,0 +1,37 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
</script>
<script lang="ts">
export default {
name: 'SharedView',
}
</script>
<template>
<a-layout id="nc-app">
<a-layout class="!flex-col">
<a-layout-header class="flex !bg-primary items-center text-white pl-3 pr-4 shadow-lg">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105" @click="navigateTo('/')">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="flex-1" />
</a-layout-header>
<div class="w-full overflow-hidden" style="height: calc(100% - var(--header-height))">
<slot />
</div>
</a-layout>
</a-layout>
</template>
<style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

15
packages/nc-gui-v2/lib/types.ts

@ -1,5 +1,5 @@
import type { FilterType } from 'nocodb-sdk'
import type { Role } from './enums'
export interface User {
id: string
email: string
@ -10,4 +10,17 @@ export interface User {
project_id?: string
}
export interface ProjectMetaInfo {
Node?: string
Arch?: string
Platform?: string
Docker?: boolean
Database?: string
ProjectOnRootDB?: string
RootDB?: string
PackageVersion?: string
}
export type Roles = Record<Role, boolean>
export type Filter = FilterType & { status?: 'update' | 'delete' | 'create'; parentId?: string; readOnly?: boolean }

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

@ -1,10 +1,12 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import {
navigateTo,
onKeyStroke,
openLink,
provideSidebar,
ref,
useClipboard,
useElementHover,
useProject,
useRoute,
@ -12,15 +14,18 @@ import {
useUIPermission,
} from '#imports'
import { TabType } from '~/composables'
const route = useRoute()
const { project, loadProject, loadTables, isSharedBase } = useProject()
const { appInfo, token } = useGlobal()
const { project, loadProject, loadTables, isSharedBase, loadProjectMetaInfo, projectMetaInfo } = useProject()
const { addTab, clearTabs } = useTabs()
const { isUIAllowed } = useUIPermission()
const { copy } = useClipboard()
// create a new sidebar state
const { isOpen, toggle } = provideSidebar({ isOpen: true })
@ -57,6 +62,31 @@ await loadProject()
await loadTables()
const isHovered = useElementHover(sidebar)
const copyProjectInfo = async () => {
try {
await loadProjectMetaInfo()
copy(
Object.entries(projectMetaInfo.value!)
.map(([k, v]) => `${k}: **${v}**`)
.join('\n'),
)
message.info('Copied project info to clipboard')
} catch (e: any) {
console.log(e)
message.error(e.message)
}
}
const copyAuthToken = async () => {
try {
copy(token.value!)
message.info('Copied auth token to clipboard')
} catch (e: any) {
console.log(e)
message.error(e.message)
}
}
</script>
<template>
@ -104,7 +134,7 @@ const isHovered = useElementHover(sidebar)
</template>
</div>
<a-dropdown v-else :trigger="['click']" placement="bottom">
<a-dropdown v-else class="h-full" :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"
@ -141,71 +171,44 @@ const isHovered = useElementHover(sidebar)
</template>
<a-menu-item key="copy">
<div class="nc-project-menu-item group">
<div class="nc-project-menu-item group" @click.stop="copyProjectInfo">
<MdiContentCopy class="group-hover:text-pink-500 nc-copy-project-info" />
Copy Project Info
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="api">
<div
v-if="isUIAllowed('apiDocs')"
v-t="['e:api-docs']"
class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`)"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
>
<MdiApi class="group-hover:text-pink-500 nc-swagger-api-docs" />
Swagger: Rest APIs
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="teamAndAuth">
<div
v-if="isUIAllowed('teamAndAuth')"
v-t="['c:navdraw:team-and-auth']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')"
>
<MdiAccountGroup class="group-hover:text-pink-500 nc-team-and-auth" />
Team & Auth
</div>
</a-menu-item>
<a-menu-item key="appStore">
<div
v-if="isUIAllowed('appStore')"
v-t="['c:navdraw:app-store']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'appStore')"
>
<MdiStore class="group-hover:text-pink-500 nc-app-store" />
App Store
<a-menu-item key="copy">
<div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken">
<MdiScriptTextKeyOutline class="group-hover:text-pink-500 nc-copy-project-info" />
Copy Auth Token
</div>
</a-menu-item>
<a-menu-item key="metaData">
<div
v-if="isUIAllowed('projectMetadata')"
v-t="['c:navdraw:project-metadata']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'metaData')"
>
<MdiTableBorder class="group-hover:text-pink-500 nc-meta-data" />
Project Metadata
</div>
</a-menu-item>
<a-menu-divider />
<a-menu-item key="audit">
<a-menu-item key="teamAndSettings">
<div
v-if="isUIAllowed('audit')"
v-t="['c:navdraw:audit']"
v-if="isUIAllowed('settings')"
v-t="['c:navdraw:project-settings']"
class="nc-project-menu-item group"
@click="toggleDialog(true, 'audit')"
@click="toggleDialog(true, 'teamAndAuth')"
>
<MdiNotebookCheckOutline class="group-hover:text-pink-500 nc-audit" />
Audit
<MdiCog class="group-hover:text-pink-500 nc-team-settings" />
Team & Settings
</div>
</a-menu-item>
@ -214,7 +217,7 @@ const isHovered = useElementHover(sidebar)
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as" v-t="['c:navdraw:preview-as']">
<template #title>
<div class="nc-project-menu-item group">
<MdiContentCopy class="group-hover:text-pink-500 nc-project-preview" />
<MdiFileEyeOutline class="group-hover:text-pink-500 nc-project-preview" />
Preview Project As
<div class="flex-1" />

39
packages/nc-gui-v2/pages/[projectType]/form/[viewId].vue

@ -0,0 +1,39 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReloadViewDataHookInj, useRoute } from '#imports'
definePageMeta({
requiresAuth: false,
})
const route = useRoute()
const reloadEventHook = createEventHook<void>()
const { sharedView, loadSharedView, meta, formColumns } = useSharedView()
await loadSharedView(route.params.viewId as string)
provide(ReloadViewDataHookInj, reloadEventHook)
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, formColumns)
provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta)
</script>
<template>
<NuxtLayout id="content" class="flex">
<div class="nc-container flex flex-col h-full mt-2 px-6">
<SharedViewForm />
</div>
</NuxtLayout>
</template>
<style scoped>
.nc-container {
height: calc(100% - var(--header-height));
flex: 1 1 100%;
}
</style>

32
packages/nc-gui-v2/pages/[projectType]/view/[viewId].vue

@ -0,0 +1,32 @@
<script setup lang="ts">
import { ReadonlyInj, ReloadViewDataHookInj, useRoute } from '#imports'
definePageMeta({
requiresAuth: false,
layout: 'shared-view',
})
const route = useRoute()
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, ref(true))
const { loadSharedView } = useSharedView()
const showPassword = ref(false)
try {
await loadSharedView(route.params.viewId as string)
} catch (e) {
showPassword.value = true
}
</script>
<template>
<NuxtLayout id="content" class="flex" name="shared-view">
<div v-if="showPassword">
<SharedViewAskPassword v-model="showPassword" />
</div>
<SharedViewGrid v-else />
</NuxtLayout>
</template>

6
packages/nc-gui-v2/utils/NcAutocompleteTree.ts

@ -37,6 +37,7 @@ export class NcAutocompleteTree {
}
root.value = root.value || []
root.value.push(word)
root.isLeaf = true
}
find(word: string) {
@ -48,12 +49,11 @@ export class NcAutocompleteTree {
return null // if not found return null
}
}
return root // return the root where it ends search
}
traverse(root: Node) {
if (root.value && root.value.length) {
if (root.isLeaf) {
this.suggestions.push(...root.value)
}
@ -70,6 +70,8 @@ export class NcAutocompleteTree {
return this.suggestions
} // cannot suggest anything
this.suggestions.push(...root.value)
const children = root.children
let spread = 0

5
packages/nc-gui-v2/utils/urlUtils.ts

@ -24,6 +24,7 @@ export const isValidURL = (str: string) => {
return !!pattern.test(str)
}
export const openLink = (url: string, target = '_blank') => {
window.open(url, target)
export const openLink = (path: string, baseURL: string, target = '_blank') => {
const url = new URL(path, baseURL)
window.open(url.href, target)
}

Loading…
Cancel
Save