mirror of https://github.com/nocodb/nocodb
Anbarasu
21 hours ago
committed by
GitHub
32 changed files with 593 additions and 234 deletions
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 744 B |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
@ -1,82 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface' |
||||
import { onMounted } from '@vue/runtime-core' |
||||
|
||||
const { includeM2M, showNull } = useGlobal() |
||||
|
||||
const baseStore = useBase() |
||||
const basesStore = useBases() |
||||
const { loadTables, hasEmptyOrNullFilters } = baseStore |
||||
const { base } = storeToRefs(baseStore) |
||||
const _projectId = inject(ProjectIdInj, undefined) |
||||
const baseId = computed(() => _projectId?.value ?? base.value?.id) |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
watch(includeM2M, async () => await loadTables()) |
||||
|
||||
const showNullAndEmptyInFilter = ref() |
||||
|
||||
onMounted(async () => { |
||||
await basesStore.loadProject(baseId.value!, true) |
||||
showNullAndEmptyInFilter.value = basesStore.getProjectMeta(baseId.value!)?.showNullAndEmptyInFilter |
||||
}) |
||||
|
||||
async function showNullAndEmptyInFilterOnChange(evt: CheckboxChangeEvent) { |
||||
const base = basesStore.bases.get(baseId.value!) |
||||
if (!base) throw new Error(`Base ${baseId.value} not found`) |
||||
|
||||
const meta = basesStore.getProjectMeta(baseId.value!) ?? {} |
||||
|
||||
// users cannot hide null & empty option if there is existing null / empty filters |
||||
if (!evt.target.checked) { |
||||
if (await hasEmptyOrNullFilters()) { |
||||
showNullAndEmptyInFilter.value = true |
||||
message.warning(t('msg.error.nullFilterExists')) |
||||
} |
||||
} |
||||
const newProjectMeta = { |
||||
...meta, |
||||
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value, |
||||
} |
||||
// update local state |
||||
base.meta = newProjectMeta |
||||
// update db |
||||
await basesStore.updateProject(baseId.value!, { |
||||
meta: JSON.stringify(newProjectMeta), |
||||
}) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex flex-row w-full"> |
||||
<div class="flex flex-col w-full"> |
||||
<div class="flex flex-row items-center w-full mb-4 gap-2"> |
||||
<!-- Show M2M Tables --> |
||||
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc"> |
||||
{{ $t('msg.info.showM2mTables') }} <br /> |
||||
<span class="text-gray-500">{{ $t('msg.info.showM2mTablesDesc') }}</span> |
||||
</a-checkbox> |
||||
</div> |
||||
<div class="flex flex-row items-center w-full mb-4 gap-2"> |
||||
<!-- Show NULL --> |
||||
<a-checkbox v-model:checked="showNull" v-e="['c:settings:show-null']" class="nc-settings-show-null"> |
||||
{{ $t('msg.info.showNullInCells') }} <br /> |
||||
<span class="text-gray-500">{{ $t('msg.info.showNullInCellsDesc') }}</span> |
||||
</a-checkbox> |
||||
</div> |
||||
<div class="flex flex-row items-center w-full mb-4 gap-2"> |
||||
<!-- Show NULL and EMPTY in Filters --> |
||||
<a-checkbox |
||||
v-model:checked="showNullAndEmptyInFilter" |
||||
v-e="['c:settings:show-null-and-empty-in-filter']" |
||||
class="nc-settings-show-null-and-empty-in-filter" |
||||
@change="showNullAndEmptyInFilterOnChange" |
||||
> |
||||
{{ $t('msg.info.showNullAndEmptyInFilter') }} <br /> |
||||
<span class="text-gray-500">{{ $t('msg.info.showNullAndEmptyInFilterDesc') }}</span> |
||||
</a-checkbox> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,7 @@
|
||||
<script setup lang="ts"></script> |
||||
|
||||
<template> |
||||
<div></div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"></style> |
@ -0,0 +1,102 @@
|
||||
<script setup lang="ts"> |
||||
const { t } = useI18n() |
||||
|
||||
const baseStore = useBase() |
||||
const basesStore = useBases() |
||||
const { base } = storeToRefs(baseStore) |
||||
|
||||
const _projectId = inject(ProjectIdInj, undefined) |
||||
const { loadTables, hasEmptyOrNullFilters } = baseStore |
||||
|
||||
const baseId = computed(() => _projectId?.value ?? base.value?.id) |
||||
|
||||
const showNullAndEmptyInFilter = ref() |
||||
|
||||
const { includeM2M, showNull } = useGlobal() |
||||
|
||||
watch(includeM2M, async () => await loadTables()) |
||||
|
||||
onMounted(async () => { |
||||
await basesStore.loadProject(baseId.value!, true) |
||||
showNullAndEmptyInFilter.value = basesStore.getProjectMeta(baseId.value!)?.showNullAndEmptyInFilter |
||||
}) |
||||
|
||||
async function showNullAndEmptyInFilterOnChange(evt: boolean) { |
||||
const base = basesStore.bases.get(baseId.value!) |
||||
if (!base) throw new Error(`Base ${baseId.value} not found`) |
||||
|
||||
const meta = basesStore.getProjectMeta(baseId.value!) ?? {} |
||||
|
||||
// users cannot hide null & empty option if there is existing null / empty filters |
||||
if (!evt) { |
||||
if (await hasEmptyOrNullFilters()) { |
||||
showNullAndEmptyInFilter.value = true |
||||
message.warning(t('msg.error.nullFilterExists')) |
||||
} |
||||
} |
||||
const newProjectMeta = { |
||||
...meta, |
||||
showNullAndEmptyInFilter: showNullAndEmptyInFilter.value, |
||||
} |
||||
// update local state |
||||
base.meta = newProjectMeta |
||||
// update db |
||||
await basesStore.updateProject(baseId.value!, { |
||||
meta: JSON.stringify(newProjectMeta), |
||||
}) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div data-testid="nc-settings-subtab-visibility" class="item-card flex flex-col w-full"> |
||||
<div class="text-nc-content-gray-emphasis font-semibold text-lg"> |
||||
{{ $t('labels.visibilityAndDataHandling') }} |
||||
</div> |
||||
|
||||
<div class="text-nc-content-gray-subtle2 mt-2 leading-5"> |
||||
{{ $t('labels.visibilityConfigLabel') }} |
||||
</div> |
||||
|
||||
<div class="flex flex-col border-1 rounded-lg mt-6 border-nc-border-gray-medium"> |
||||
<div class="flex w-full px-3 py-2 gap-2 flex-col"> |
||||
<div class="flex w-full gap-1 items-center"> |
||||
<NcSwitch v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc-m2m"> |
||||
<span class="text-nc-content-gray font-semibold flex-1"> |
||||
{{ $t('msg.info.showM2mTables') }} |
||||
</span> |
||||
</NcSwitch> |
||||
</div> |
||||
<span class="text-gray-500 pl-10">{{ $t('msg.info.showM2mTablesDesc') }}</span> |
||||
</div> |
||||
|
||||
<div class="flex w-full px-3 border-t-1 border-nc-border-gray-medium py-2 gap-2 flex-col"> |
||||
<div class="flex w-full gap-1 items-center"> |
||||
<NcSwitch v-model:checked="showNull" v-e="['c:settings:show-null']" class="nc-settings-show-null"> |
||||
<span class="text-nc-content-gray font-semibold flex-1"> |
||||
{{ $t('msg.info.showNullInCells') }} |
||||
</span> |
||||
</NcSwitch> |
||||
</div> |
||||
<span class="text-gray-500 pl-10">{{ $t('msg.info.showNullInCellsDesc') }}</span> |
||||
</div> |
||||
|
||||
<div class="flex w-full px-3 py-2 border-t-1 border-nc-border-gray-medium gap-2 flex-col"> |
||||
<div class="flex w-full gap-1 items-center"> |
||||
<NcSwitch |
||||
v-model:checked="showNullAndEmptyInFilter" |
||||
v-e="['c:settings:show-null-and-empty-in-filter']" |
||||
class="nc-settings-show-null-and-empty-in-filter" |
||||
@change="showNullAndEmptyInFilterOnChange" |
||||
> |
||||
<span class="text-nc-content-gray font-semibold flex-1"> |
||||
{{ $t('msg.info.showNullAndEmptyInFilter') }} |
||||
</span> |
||||
</NcSwitch> |
||||
</div> |
||||
<span class="text-gray-500 pl-10">{{ $t('msg.info.showNullAndEmptyInFilterDesc') }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"></style> |
@ -0,0 +1,84 @@
|
||||
<script setup lang="ts"> |
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
const hasPermissionForSnapshots = computed(() => isUIAllowed('manageSnapshot')) |
||||
|
||||
const { isFeatureEnabled } = useBetaFeatureToggle() |
||||
|
||||
const router = useRouter() |
||||
|
||||
const activeMenu = ref( |
||||
isEeUI && isFeatureEnabled(FEATURE_FLAG.BASE_SNAPSHOTS) && hasPermissionForSnapshots.value ? 'snapshots' : 'visibility', |
||||
) |
||||
|
||||
const selectMenu = (option: string) => { |
||||
if (!hasPermissionForSnapshots.value && option === 'snapshots') { |
||||
return |
||||
} |
||||
router.push({ |
||||
query: { |
||||
...router.currentRoute.value.query, |
||||
tab: option, |
||||
}, |
||||
}) |
||||
activeMenu.value = option |
||||
} |
||||
|
||||
onMounted(() => { |
||||
const query = router.currentRoute.value.query |
||||
if (query && query.tab && ['snapshots', 'visibility'].includes(query.tab as string)) { |
||||
selectMenu(query.tab as string) |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="flex p-5 nc-base-settings justify-center overflow-auto gap-8"> |
||||
<!-- Left Pane --> |
||||
<div class="flex flex-col"> |
||||
<div class="h-full w-60"> |
||||
<div |
||||
v-if="isEeUI && hasPermissionForSnapshots && isFeatureEnabled(FEATURE_FLAG.BASE_SNAPSHOTS)" |
||||
data-testid="snapshots-tab" |
||||
:class="{ |
||||
'active-menu': activeMenu === 'snapshots', |
||||
}" |
||||
class="gap-3 !hover:bg-gray-50 transition-all text-nc-content-gray flex rounded-lg items-center cursor-pointer py-1.5 px-3" |
||||
@click="selectMenu('snapshots')" |
||||
> |
||||
<GeneralIcon icon="camera" /> |
||||
|
||||
<span> |
||||
{{ $t('general.snapshots') }} |
||||
</span> |
||||
</div> |
||||
|
||||
<div |
||||
:class="{ |
||||
'active-menu': activeMenu === 'visibility', |
||||
}" |
||||
class="gap-3 !hover:bg-gray-50 transition-all text-nc-content-gray flex rounded-lg items-center cursor-pointer py-1.5 px-3" |
||||
data-testid="visibility-tab" |
||||
@click="selectMenu('visibility')" |
||||
> |
||||
<GeneralIcon icon="ncEye" /> |
||||
<span> |
||||
{{ $t('labels.visibilityAndDataHandling') }} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<!-- Data Pane --> |
||||
|
||||
<div class="flex flex-col w-[760px]"> |
||||
<DashboardSettingsBaseSnapshots v-if="activeMenu === 'snapshots'" /> |
||||
<DashboardSettingsBaseVisibility v-if="activeMenu === 'visibility'" /> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style lang="scss" scoped> |
||||
.active-menu { |
||||
@apply !bg-brand-50 font-semibold !text-nc-content-brand-disabled; |
||||
} |
||||
</style> |
@ -1,22 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
const { isUIAllowed } = useRoles() |
||||
|
||||
const { t } = useI18n() |
||||
|
||||
const toggleDialog = inject(ToggleDialogInj, () => {}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
v-if="isUIAllowed('settingsPage')" |
||||
class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)" |
||||
@click="toggleDialog(true, undefined, undefined, baseId)" |
||||
> |
||||
<div> |
||||
<div class="flex items-center space-x-1"> |
||||
<component :is="iconMap.users" class="mr-1 nc-new-source" /> |
||||
<div>{{ t('title.teamAndSettings') }}</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
@ -0,0 +1,57 @@
|
||||
import BasePage from '../../Base'; |
||||
import { ProjectViewPage } from './index'; |
||||
import { expect } from '@playwright/test'; |
||||
|
||||
export class BaseSettingsPage extends BasePage { |
||||
readonly dashboard: DashboardPage; |
||||
readonly baseView: ProjectViewPage; |
||||
|
||||
constructor(baseView: ProjectViewPage) { |
||||
super(baseView.rootPage); |
||||
|
||||
this.baseView = baseView; |
||||
} |
||||
|
||||
get() { |
||||
return this.baseView.get().locator('.nc-base-settings'); |
||||
} |
||||
|
||||
async changeTab(tabName: 'snapshots' | 'visibility') { |
||||
await this.get().getByTestId(`${tabName}-tab`).click(); |
||||
|
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
|
||||
async createSnapshot({ snapshotName }: { snapshotName: string }) { |
||||
await this.rootPage.getByTestId('add-new-snapshot').click(); |
||||
|
||||
await this.rootPage.waitForTimeout(1000); |
||||
|
||||
await this.rootPage.locator('.new-snapshot-title').fill(snapshotName); |
||||
|
||||
await this.rootPage.getByTestId('create-snapshot-btn').click(); |
||||
|
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
|
||||
async deleteSnapshot({ snapshotName }: { snapshotName: string }) { |
||||
await this.rootPage.getByTestId(`snapshot-${snapshotName}`).getByTestId('delete-snapshot-btn').click(); |
||||
await this.rootPage.getByTestId('nc-delete-modal-delete-btn').click(); |
||||
await this.rootPage.waitForTimeout(1000); |
||||
} |
||||
|
||||
async restoreSnapshot({ snapshotName }: { snapshotName: string }) { |
||||
await this.rootPage.getByTestId(`snapshot-${snapshotName}`).getByTestId('restore-snapshot-btn').click(); |
||||
await this.rootPage.getByTestId('confirm-restore-snapshot-btn').click(); |
||||
await this.rootPage.waitForTimeout(3000); |
||||
} |
||||
|
||||
async verifySnapshot({ snapshotName, isVisible }: { snapshotName: string; isVisible: boolean }) { |
||||
const snapshot = this.rootPage.getByTestId(`snapshot-${snapshotName}`); |
||||
if (isVisible) { |
||||
await expect(snapshot).toBeVisible({ visible: true }); |
||||
} else { |
||||
await expect(snapshot).toBeVisible({ visible: false }); |
||||
} |
||||
} |
||||
} |
Loading…
Reference in new issue