mirror of https://github.com/nocodb/nocodb
Wing-Kam Wong
2 years ago
113 changed files with 2413 additions and 1135 deletions
@ -0,0 +1,9 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
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" |
||||||
|
>Star</GithubButton |
||||||
|
> |
||||||
|
</template> |
@ -0,0 +1,16 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const { includeM2M } = useGlobal() |
||||||
|
const { loadTables } = useProject() |
||||||
|
|
||||||
|
watch(includeM2M, async () => await loadTables()) |
||||||
|
</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-checkbox v-model:checked="includeM2M">Show M2M Tables</a-checkbox> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,69 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { extractSdkResponseErrorMsg, onMounted } from '#imports' |
||||||
|
|
||||||
|
const { $api } = useNuxtApp() |
||||||
|
|
||||||
|
const { currentVersion, latestRelease, hiddenRelease } = useGlobal() |
||||||
|
|
||||||
|
const releaseAlert = computed( |
||||||
|
() => |
||||||
|
currentVersion.value && |
||||||
|
latestRelease.value && |
||||||
|
currentVersion.value !== latestRelease.value && |
||||||
|
latestRelease.value !== hiddenRelease.value, |
||||||
|
) |
||||||
|
|
||||||
|
async function fetchReleaseInfo() { |
||||||
|
try { |
||||||
|
const versionInfo = await $api.utils.appVersion() |
||||||
|
if (versionInfo && versionInfo.releaseVersion && versionInfo.currentVersion && !/[^0-9.]/.test(versionInfo.currentVersion)) { |
||||||
|
currentVersion.value = versionInfo.currentVersion |
||||||
|
latestRelease.value = versionInfo.releaseVersion |
||||||
|
} else { |
||||||
|
currentVersion.value = null |
||||||
|
latestRelease.value = null |
||||||
|
} |
||||||
|
} catch (e: any) { |
||||||
|
message.error(await extractSdkResponseErrorMsg(e)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(async () => await fetchReleaseInfo()) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div v-if="releaseAlert" class="flex items-center"> |
||||||
|
<a-dropdown :trigger="['click']" placement="bottom"> |
||||||
|
<a-button class="bg-primary border-none"> |
||||||
|
<div class="flex gap-1 align-center text-white"> |
||||||
|
<span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span> |
||||||
|
<mdi-menu-down /> |
||||||
|
</div> |
||||||
|
</a-button> |
||||||
|
<template #overlay> |
||||||
|
<div class="mt-1 bg-white shadow-lg !border"> |
||||||
|
<nuxt-link class="text-primary" to="https://github.com/nocodb/nocodb/releases" target="_blank"> |
||||||
|
<div class="nc-menu-item"> |
||||||
|
<mdi-script-text-outline /> |
||||||
|
{{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }} |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
<nuxt-link class="text-primary" to="https://docs.nocodb.com/getting-started/upgrading" target="_blank"> |
||||||
|
<div class="nc-menu-item"> |
||||||
|
<mdi-rocket-launch-outline /> |
||||||
|
<!-- How to upgrade? --> |
||||||
|
{{ $t('activity.upgrade.howTo') }} |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
<a-divider class="ma-0" /> |
||||||
|
<div class="nc-menu-item" @click="latestRelease = null"> |
||||||
|
<mdi-close /> |
||||||
|
<!-- Hide menu --> |
||||||
|
{{ $t('general.hideMenu') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</a-dropdown> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,33 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { useRoute } from '#imports' |
||||||
|
|
||||||
|
const route = useRoute() |
||||||
|
|
||||||
|
const showUserModal = $ref(false) |
||||||
|
|
||||||
|
const { isUIAllowed } = useUIPermission() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex items-center mr-4"> |
||||||
|
<a-button |
||||||
|
v-if=" |
||||||
|
isUIAllowed('newUser') && |
||||||
|
route.name !== 'index' && |
||||||
|
route.name !== 'project-index-create' && |
||||||
|
route.name !== 'project-index-create-external' && |
||||||
|
route.name !== 'index-user-index' |
||||||
|
" |
||||||
|
size="middle" |
||||||
|
type="primary" |
||||||
|
class="!bg-white !text-primary rounded" |
||||||
|
@click="showUserModal = true" |
||||||
|
> |
||||||
|
<div class="flex items-center space-x-1"> |
||||||
|
<mdi-account-supervisor-outline class="mr-1" /> |
||||||
|
<div>{{ $t('activity.share') }}</div> |
||||||
|
</div> |
||||||
|
</a-button> |
||||||
|
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" /> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,81 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { enumColor as colors } from '#imports' |
||||||
|
|
||||||
|
const { lang: currentLang } = useGlobal() |
||||||
|
|
||||||
|
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value)) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-card :body-style="{ padding: '0' }" class="w-[300px] shadow-sm rounded-lg"> |
||||||
|
<a-list class="w-full" dense> |
||||||
|
<a-list-item> |
||||||
|
<nuxt-link class="text-primary" to="https://github.com/nocodb/nocodb" target="_blank"> |
||||||
|
<div class="flex items-center text-sm"> |
||||||
|
<mdi-github class="mx-3 text-lg" /> |
||||||
|
<div v-if="isRtlLang"> |
||||||
|
<!-- us on Github --> |
||||||
|
{{ $t('labels.community.starUs2') }} |
||||||
|
<!-- Star --> |
||||||
|
{{ $t('labels.community.starUs1') }} |
||||||
|
<mdi-star-outline /> |
||||||
|
</div> |
||||||
|
<div v-else class="flex items-center"> |
||||||
|
<!-- Star --> |
||||||
|
{{ $t('labels.community.starUs1') }} |
||||||
|
<mdi-star-outline class="mx-1" /> |
||||||
|
<!-- us on Github --> |
||||||
|
{{ $t('labels.community.starUs2') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
</a-list-item> |
||||||
|
<a-list-item> |
||||||
|
<nuxt-link class="text-primary" to="https://calendly.com/nocodb-meeting" target="_blank"> |
||||||
|
<div class="flex items-center text-sm"> |
||||||
|
<mdi-calendar-month class="mx-3 text-lg" :color="colors.dark[3 % colors.dark.length]" /> |
||||||
|
<!-- Book a Free DEMO --> |
||||||
|
<div> |
||||||
|
{{ $t('labels.community.bookDemo') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
</a-list-item> |
||||||
|
<a-list-item> |
||||||
|
<nuxt-link class="text-primary" to="https://discord.gg/5RgZmkW" target="_blank"> |
||||||
|
<div class="flex items-center text-sm"> |
||||||
|
<mdi-discord class="mx-3 text-lg" :color="colors.dark[0 % colors.dark.length]" /> |
||||||
|
<!-- Get your questions answered --> |
||||||
|
<div> |
||||||
|
{{ $t('labels.community.getAnswered') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
</a-list-item> |
||||||
|
<a-list-item> |
||||||
|
<nuxt-link class="text-primary" to="https://twitter.com/NocoDB" target="_blank"> |
||||||
|
<div class="flex items-center text-sm"> |
||||||
|
<mdi-twitter class="mx-3 text-lg" :color="colors.dark[1 % colors.dark.length]" /> |
||||||
|
<!-- Follow NocoDB --> |
||||||
|
<div> |
||||||
|
{{ $t('labels.community.followNocodb') }} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
</a-list-item> |
||||||
|
<a-list-item v-t="['e:hiring']"> |
||||||
|
<nuxt-link class="text-primary" target="_blank" to="http://careers.nocodb.com"> |
||||||
|
<div class="flex items-center text-sm"> |
||||||
|
<div class="ml-3">🚀 <span class="ml-2">We are Hiring!!!</span></div> |
||||||
|
</div> |
||||||
|
</nuxt-link> |
||||||
|
</a-list-item> |
||||||
|
</a-list> |
||||||
|
</a-card> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
:deep(.ant-list-item) { |
||||||
|
@apply hover:(bg-gray-100 !text-primary); |
||||||
|
} |
||||||
|
</style> |
@ -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> |
@ -0,0 +1,85 @@ |
|||||||
|
<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" :level="1"> |
||||||
|
{{ sharedView.view.heading }} |
||||||
|
</a-typography-title> |
||||||
|
<a-typography class="pl-1 text-sm">{{ 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" |
||||||
|
: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" |
||||||
|
: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> |
@ -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> |
@ -0,0 +1,28 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const editorOpen = ref(false) |
||||||
|
|
||||||
|
const tabKey = ref() |
||||||
|
|
||||||
|
const { metas } = $(useMetas()) |
||||||
|
|
||||||
|
const { tables } = useTable() |
||||||
|
|
||||||
|
const localTables = tables.value.filter((t) => metas[t.id as string]) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-tooltip placement="bottom"> |
||||||
|
<template #title> |
||||||
|
<span> Debug Meta </span> |
||||||
|
</template> |
||||||
|
<mdi-bug-outline class="cursor-pointer" @click="editorOpen = true" /> |
||||||
|
</a-tooltip> |
||||||
|
|
||||||
|
<a-modal v-model:visible="editorOpen" :footer="null" width="80%"> |
||||||
|
<a-tabs v-model:activeKey="tabKey" type="card" closeable="false" class="shadow-sm"> |
||||||
|
<a-tab-pane v-for="table in localTables" :key="table.id" :tab="table.title"> |
||||||
|
<MonacoEditor v-model="metas[table.id]" class="h-max-[70vh]" :read-only="true" /> |
||||||
|
</a-tab-pane> |
||||||
|
</a-tabs> |
||||||
|
</a-modal> |
||||||
|
</template> |
@ -0,0 +1,22 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
const { api } = useApi() |
||||||
|
|
||||||
|
async function deleteCache() { |
||||||
|
try { |
||||||
|
await api.utils.cacheDelete() |
||||||
|
message.info('Deleted Cache Successfully') |
||||||
|
} catch (e: any) { |
||||||
|
message.error(e.message) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-tooltip placement="bottom"> |
||||||
|
<template #title> |
||||||
|
<span> Delete Cache </span> |
||||||
|
</template> |
||||||
|
<mdi-delete class="cursor-pointer" @click="deleteCache" /> |
||||||
|
</a-tooltip> |
||||||
|
</template> |
@ -0,0 +1,32 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import FileSaver from 'file-saver' |
||||||
|
|
||||||
|
const { api } = useApi() |
||||||
|
|
||||||
|
async function exportCache() { |
||||||
|
try { |
||||||
|
const data = await api.utils.cacheGet() |
||||||
|
if (!data) { |
||||||
|
message.info('Cache is empty') |
||||||
|
return |
||||||
|
} |
||||||
|
const blob = new Blob([JSON.stringify(data)], { |
||||||
|
type: 'text/plain;charset=utf-8', |
||||||
|
}) |
||||||
|
FileSaver.saveAs(blob, 'cache_exported.json') |
||||||
|
message.info('Exported Cache Successfully') |
||||||
|
} catch (e: any) { |
||||||
|
message.error(e.message) |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-tooltip placement="bottom"> |
||||||
|
<template #title> |
||||||
|
<span> Export Cache </span> |
||||||
|
</template> |
||||||
|
<mdi-export class="cursor-pointer" @click="exportCache" /> |
||||||
|
</a-tooltip> |
||||||
|
</template> |
@ -0,0 +1,66 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { onMounted } from '@vue/runtime-core' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
modelValue: Record<string, any>[] |
||||||
|
availableChannelList: Record<string, any>[] |
||||||
|
placeholder: string |
||||||
|
} |
||||||
|
|
||||||
|
const { availableChannelList, placeholder, ...rest } = defineProps<Props>() |
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']) |
||||||
|
|
||||||
|
const vModel = useVModel(rest, 'modelValue', emit) |
||||||
|
|
||||||
|
// idx of selected channels |
||||||
|
const localChannelValues = $ref<number[]>([]) |
||||||
|
|
||||||
|
// availableChannelList with idx enriched |
||||||
|
let availableChannelWithIdxList = $ref<Record<string, any>[]>() |
||||||
|
|
||||||
|
watch( |
||||||
|
() => localChannelValues, |
||||||
|
(v) => { |
||||||
|
const res = [] |
||||||
|
for (const channelIdx of v) { |
||||||
|
const target = availableChannelWithIdxList.find((availableChannel) => availableChannel.idx === channelIdx) |
||||||
|
if (target) { |
||||||
|
// push without target.idx |
||||||
|
res.push({ webhook_url: target.webhook_url, channel: target.channel }) |
||||||
|
} |
||||||
|
} |
||||||
|
vModel.value = res |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
if (availableChannelList.length) { |
||||||
|
// enrich idx |
||||||
|
let idx = 0 |
||||||
|
availableChannelWithIdxList = availableChannelList.map((channel) => ({ |
||||||
|
...channel, |
||||||
|
idx: idx++, |
||||||
|
})) |
||||||
|
|
||||||
|
// build localChannelValues from modelValue |
||||||
|
for (const channel of rest.modelValue || []) { |
||||||
|
const target = availableChannelWithIdxList.find( |
||||||
|
(availableChannelWithIdx) => |
||||||
|
availableChannelWithIdx.webhook_url === channel.webhook_url && availableChannelWithIdx.channel === channel.channel, |
||||||
|
) |
||||||
|
if (target) { |
||||||
|
localChannelValues.push(target.idx) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-select v-model:value="localChannelValues" mode="multiple" :placeholder="placeholder" max-tag-count="responsive"> |
||||||
|
<a-select-option v-for="channel of availableChannelWithIdxList" :key="channel.idx" :value="channel.idx">{{ |
||||||
|
channel.channel |
||||||
|
}}</a-select-option> |
||||||
|
</a-select> |
||||||
|
</template> |
@ -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, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
declare module 'httpsnippet' { |
||||||
|
export default new ((): any => {})() |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
declare module 'just-clone' |
@ -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> |
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue