Browse Source

Merge pull request #7625 from nocodb/nc-cmd-l

fix(nc-gui): cmd l fixes
pull/7652/head
Raju Udava 9 months ago committed by GitHub
parent
commit
070a59e270
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/nc-gui/app.vue
  2. 9
      packages/nc-gui/assets/nc-icons/project-gray.svg
  3. 218
      packages/nc-gui/components/cmd-l/index.vue
  4. 6
      packages/nc-gui/components/dlg/TableRename.vue
  5. 2
      packages/nc-gui/store/views.ts
  6. 2
      packages/nc-gui/utils/iconUtils.ts
  7. 2
      tests/playwright/pages/Dashboard/Command/CmdJPage.ts
  8. 3
      tests/playwright/pages/Dashboard/Command/CmdKPage.ts
  9. 9
      tests/playwright/pages/Dashboard/Command/CmdLPage.ts
  10. 98
      tests/playwright/tests/db/features/command.spec.ts

1
packages/nc-gui/app.vue

@ -1,6 +1,7 @@
<script setup lang="ts">
import { applyNonSelectable, computed, isEeUI, isMac, useCommandPalette, useRouter, useTheme } from '#imports'
import type { CommandPaletteType } from '~/lib'
const router = useRouter()
const route = router.currentRoute

9
packages/nc-gui/assets/nc-icons/project-gray.svg

@ -0,0 +1,9 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.99294 15.3098L14.548 12.7802C14.8177 12.6574 14.9569 12.4978 14.9659 12.3369H1C1.00896 12.4978 1.14826 12.6574 1.4179 12.7802L6.97294 15.3098C7.53075 15.5637 8.43514 15.5637 8.99294 15.3098Z" fill="#5F5F5F"/>
<path d="M14.9999 9.77881H1.00513V12.3366H14.9999V9.77881Z" fill="#5F5F5F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.99294 12.7517L14.548 10.2221C14.8177 10.0993 14.9569 9.93968 14.9659 9.77881H1C1.00896 9.93968 1.14826 10.0993 1.4179 10.2221L6.97294 12.7517C7.53075 13.0056 8.43514 13.0056 8.99294 12.7517Z" fill="#5F5F5F"/>
<path d="M14.9999 7.22119H1.00513V9.77897H14.9999V7.22119Z" fill="#5F5F5F"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.99294 11.4729L14.548 8.94332C14.8177 8.82054 14.9569 8.66088 14.9659 8.5H1C1.00896 8.66088 1.14826 8.82054 1.4179 8.94332L6.97294 11.4729C7.53075 11.7269 8.43514 11.7269 8.99294 11.4729Z" fill="#C4C4C4"/>
<path d="M14.9997 4.66309H1.00488V8.50309H14.9997V4.66309Z" fill="#C4C4C4"/>
<path d="M14.5484 5.13991L8.99337 7.66947C8.43561 7.92348 7.53121 7.92348 6.9734 7.66947L1.41836 5.13991C0.860546 4.8859 0.860546 4.47408 1.41836 4.22007L6.9734 1.69051C7.53121 1.4365 8.43561 1.4365 8.99337 1.69051L14.5484 4.22007C15.1063 4.47408 15.1063 4.8859 14.5484 5.13991Z" fill="#C4C4C4"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

218
packages/nc-gui/components/cmd-l/index.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onKeyUp, useDebounceFn, useMagicKeys, useVModel, whenever } from '@vueuse/core'
import { onClickOutside } from '#imports'
import { onKeyUp, useDebounceFn, useVModel } from '@vueuse/core'
import { iconMap, onClickOutside } from '#imports'
import type { CommandPaletteType } from '~/lib'
const props = defineProps<{
@ -12,8 +12,12 @@ const emits = defineEmits(['update:open'])
const vOpen = useVModel(props, 'open', emits)
const search = ref('')
const modalEl = ref<HTMLElement>()
const cmdInputEl = ref<HTMLInputElement>()
const { user } = useGlobal()
const viewStore = useViewsStore()
@ -22,14 +26,26 @@ const { recentViews, activeView } = storeToRefs(viewStore)
const selected: Ref<string> = ref('')
const newView: Ref<
const newView = ref<
| {
viewId: string | null
tableId: string
baseId: string
}
| undefined
> = ref()
>()
const filteredViews = computed(() => {
if (!recentViews.value) return []
const filtered = recentViews.value.filter((v) => {
if (search.value === '') return true
return v.viewName.toLowerCase().includes(search.value.toLowerCase())
})
if (filtered[0]) {
selected.value = filtered[0]?.tableID + filtered[0]?.viewName
}
return filtered
})
const changeView = useDebounceFn(
async ({ viewId, tableId, baseId }: { viewId: string | null; tableId: string; baseId: string }) => {
@ -39,36 +55,26 @@ const changeView = useDebounceFn(
200,
)
const keys = useMagicKeys()
const { current } = keys
onKeyUp('Enter', async () => {
if (vOpen.value && newView.value) {
search.value = ''
await changeView({ viewId: newView.value.viewId, tableId: newView.value.tableId, baseId: newView.value.baseId })
}
})
function scrollToTarget() {
const element = document.querySelector('.cmdk-action.selected')
const headerOffset = 45
const elementPosition = element?.getBoundingClientRect().top
const offsetPosition = elementPosition! + window.pageYOffset - headerOffset
window.scrollTo({
top: offsetPosition,
behavior: 'smooth',
})
element?.scrollIntoView()
}
const moveUp = () => {
if (!recentViews.value.length) return
const index = recentViews.value.findIndex((v) => v.tableID + v.viewName === selected.value)
if (!filteredViews.value.length) return
const index = filteredViews.value.findIndex((v) => v.tableID + v.viewName === selected.value)
if (index === 0) {
selected.value =
recentViews.value[recentViews.value.length - 1].tableID + recentViews.value[recentViews.value.length - 1].viewName
filteredViews.value[filteredViews.value.length - 1].tableID + filteredViews.value[filteredViews.value.length - 1].viewName
const cmdOption = recentViews.value[recentViews.value.length - 1]
const cmdOption = filteredViews.value[filteredViews.value.length - 1]
newView.value = {
viewId: cmdOption.viewId ?? null,
tableId: cmdOption.tableID,
@ -76,25 +82,25 @@ const moveUp = () => {
}
document.querySelector('.actions')?.scrollTo({ top: 99999, behavior: 'smooth' })
} else {
selected.value = recentViews.value[index - 1].tableID + recentViews.value[index - 1].viewName
const cmdOption = recentViews.value[index - 1]
scrollToTarget()
selected.value = filteredViews.value[index - 1].tableID + filteredViews.value[index - 1].viewName
const cmdOption = filteredViews.value[index - 1]
newView.value = {
viewId: cmdOption.viewId ?? null,
tableId: cmdOption.tableID,
baseId: cmdOption.baseId,
}
nextTick(() => scrollToTarget())
}
}
const moveDown = () => {
if (!recentViews.value.length) return
const index = recentViews.value.findIndex((v) => v.tableID + v.viewName === selected.value)
if (index === recentViews.value.length - 1) {
selected.value = recentViews.value[0].tableID + recentViews.value[0].viewName
if (!filteredViews.value.length) return
const index = filteredViews.value.findIndex((v) => v.tableID + v.viewName === selected.value)
if (index === filteredViews.value.length - 1) {
selected.value = filteredViews.value[0].tableID + filteredViews.value[0].viewName
const cmdOption = recentViews.value[0]
const cmdOption = filteredViews.value[0]
newView.value = {
viewId: cmdOption.viewId ?? null,
tableId: cmdOption.tableID,
@ -102,91 +108,74 @@ const moveDown = () => {
}
document.querySelector('.actions')?.scrollTo({ top: 0, behavior: 'smooth' })
} else {
selected.value = recentViews.value[index + 1].tableID + recentViews.value[index + 1].viewName
const cmdOption = recentViews.value[index + 1]
scrollToTarget()
selected.value = filteredViews.value[index + 1].tableID + filteredViews.value[index + 1].viewName
const cmdOption = filteredViews.value[index + 1]
newView.value = {
viewId: cmdOption.viewId ?? null,
tableId: cmdOption.tableID,
baseId: cmdOption.baseId,
}
nextTick(() => scrollToTarget())
}
}
whenever(keys['Ctrl+Shift+L'], async () => {
if (!user.value) return
vOpen.value = true
moveUp()
})
const hide = () => {
vOpen.value = false
search.value = ''
}
whenever(keys['Meta+Shift+L'], async () => {
if (!user.value) return
vOpen.value = true
moveUp()
onClickOutside(modalEl, () => {
hide()
})
whenever(keys.ctrl_l, async () => {
useEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Escape') {
hide()
} else if (e.key === 'Enter') {
if (newView.value) {
changeView({ viewId: newView.value.viewId, tableId: newView.value.tableId, baseId: newView.value.baseId })
}
} else if (e.key === 'ArrowUp') {
if (!vOpen.value) return
e.preventDefault()
moveUp()
} else if (e.key === 'ArrowDown') {
if (!vOpen.value) return
e.preventDefault()
moveDown()
} else if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 'l') {
if (!user.value) return
if (current.has('shift')) return
if (!vOpen.value) {
vOpen.value = true
moveDown()
})
whenever(keys.meta_l, async () => {
} else {
moveUp()
}
} else if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'l') {
if (!user.value) return
if (current.has('shift')) return
if (!vOpen.value) {
vOpen.value = true
moveDown()
})
whenever(keys.arrowup, () => {
if (vOpen.value) moveUp()
})
whenever(keys.arrowdown, () => {
if (vOpen.value) moveDown()
})
const hide = () => {
vOpen.value = false
} else moveDown()
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
hide()
} else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'j') {
hide()
} else if (vOpen.value) {
cmdInputEl.value?.focus()
}
whenever(keys.Escape, () => {
if (vOpen.value) hide()
})
whenever(keys.ctrl_k, () => {
if (vOpen.value) hide()
})
whenever(keys.meta_k, () => {
if (vOpen.value) hide()
})
whenever(keys.ctrl_j, () => {
if (vOpen.value) hide()
})
whenever(keys.meta_j, () => {
if (vOpen.value) hide()
})
onClickOutside(modalEl, () => {
if (vOpen.value) hide()
})
onMounted(() => {
document.querySelector('.cmdOpt-list')?.focus()
if (!activeView.value) return
const index = recentViews.value.findIndex(
(v) => v.viewName === activeView.value?.name && v.tableID === activeView.value?.tableId,
if (!activeView.value || !filteredViews.value.length) return
const index = filteredViews.value.findIndex(
(v) => v.viewName === activeView.value?.title && v.tableID === activeView.value?.fk_model_id,
)
if (index + 1 > recentViews.value.length) {
selected.value = recentViews.value[0].tableID + recentViews.value[0].viewName
if (index + 1 > filteredViews.value.length) {
selected.value = filteredViews.value[0].tableID + filteredViews.value[0].viewName
} else {
selected.value = recentViews.value[index + 1].tableID + recentViews.value[index + 1].viewName
if (!filteredViews.value[index + 1]) return
selected.value = filteredViews.value[index + 1].tableID + filteredViews.value[index + 1].viewName
}
})
</script>
@ -194,45 +183,64 @@ onMounted(() => {
<template>
<div v-if="vOpen" class="cmdk-modal cmdl-modal" :class="{ 'cmdk-modal-active cmdl-modal-active': vOpen }">
<div ref="modalEl" class="cmdk-modal-content cmdl-modal-content relative h-[25.25rem]">
<div class="cmdk-input-wrapper">
<GeneralIcon class="h-4 w-4 text-gray-500" icon="search" />
<input ref="cmdInputEl" v-model="search" class="cmdk-input" placeholder="Search" type="text" />
</div>
<div class="flex items-center bg-white w-full z-[50]">
<div class="text-sm p-4 text-gray-500">Recent Views</div>
<div class="text-sm px-4 py-2 text-gray-500">Recent Views</div>
</div>
<div class="flex flex-col shrink grow overflow-hidden shadow-[rgb(0_0_0_/_50%)_0px_16px_70px] max-w-[650px] p-0">
<div class="scroll-smooth actions overflow-auto nc-scrollbar-md relative m-0 px-0 py-2">
<div v-if="recentViews.length < 1" class="flex flex-col p-4 items-start justify-center text-md">No recent views</div>
<div v-else class="flex mb-10 flex-col cmdOpt-list w-full">
<div class="scroll-smooth actions overflow-auto nc-scrollbar-md mb-10 relative mx-0 px-0 py-2">
<div v-if="filteredViews.length < 1" class="flex flex-col p-4 items-start justify-center text-md">No recent views</div>
<div v-else class="flex flex-col cmdOpt-list w-full">
<div
v-for="cmdOption of recentViews"
v-for="cmdOption of filteredViews"
:key="cmdOption.tableID + cmdOption.viewName"
v-e="['a:cmdL:changeView']"
:class="{
selected: selected === cmdOption.tableID + cmdOption.viewName,
}"
class="cmdk-action"
@click="changeView({ viewId: cmdOption.viewId, tableId: cmdOption.tableID, baseId: cmdOption.baseId })"
@click="changeView({ viewId: cmdOption.viewId!, tableId: cmdOption.tableID, baseId: cmdOption.baseId })"
>
<div class="cmdk-action-content !flex w-full">
<div class="flex gap-2 w-full flex-grow-1 items-center">
<GeneralViewIcon :meta="{ type: cmdOption.viewType }" />
<div class="cmdk-action-content">
<div class="flex w-1/2 items-center">
<div class="flex gap-2">
<GeneralViewIcon :meta="{ type: cmdOption.viewType }" class="mt-0.5" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.isDefault ? $t('title.defaultView') : cmdOption.viewName }}
{{ cmdOption.viewName }}
</template>
<span class="max-w- truncate capitalize">
{{ cmdOption.isDefault ? $t('title.defaultView') : cmdOption.viewName }}
<span class="truncate max-w-56 capitalize">
{{ cmdOption.viewName }}
</span>
</a-tooltip>
</div>
<div class="flex gap-2 bg-gray-100 px-2 py-1 rounded-md text-gray-600 items-center">
<component :is="iconMap.project" class="w-4 h-4 text-transparent" />
</div>
<div class="flex w-1/2 justify-end text-gray-600">
<div class="flex gap-2 px-2 py-1 rounded-md items-center">
<component :is="iconMap.projectGray" class="w-3 h-3 text-transparent" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.baseName }}
</template>
<span class="max-w-32 truncate capitalize">
<span class="max-w-32 text-xs truncate capitalize">
{{ cmdOption.baseName }}
</span>
</a-tooltip>
<span class="text-bold"> / </span>
<component :is="iconMap.table" class="w-3 h-3 text-transparent" />
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ cmdOption.tableName }}
</template>
<span class="max-w-28 text-xs truncate capitalize">
{{ cmdOption.tableName }}
</span>
</a-tooltip>
</div>
</div>
</div>
</div>

6
packages/nc-gui/components/dlg/TableRename.vue

@ -162,8 +162,10 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
// update recent views if default view is renamed
allRecentViews.value = allRecentViews.value.map((v) => {
if (v.tableID === tableMeta.id && v.isDefault) {
v.viewName = formState.title
if (v.tableID === tableMeta.id) {
if (v.isDefault) v.viewName = formState.title
v.tableName = formState.title
}
return v
})

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

@ -11,6 +11,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
tableID: string
isDefault: boolean
baseName: string
tableName: string
workspaceId: string
baseId: string
}
@ -293,6 +294,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
viewName: view.is_default ? (tableName as string) : view.title,
viewType: view.type,
workspaceId: activeWorkspaceId.value,
tableName: tableName as string,
baseName: baseName as string,
},
...allRecentViews.value.filter((f) => f.viewId !== view.id || f.tableID !== view.fk_model_id),

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

@ -120,6 +120,7 @@ import NcArrowUp from '~icons/nc-icons/arrow-up'
import NcArrowDown from '~icons/nc-icons/arrow-down'
import NcUpload from '~icons/nc-icons/upload'
import NcDownload from '~icons/nc-icons/download'
import NcProjectGray from '~icons/nc-icons/project-gray'
// keep it for reference
// todo: remove it after all icons are migrated
@ -269,6 +270,7 @@ import NcDownload from '~icons/nc-icons/download'
} as const */
export const iconMap = {
projectGray: NcProjectGray,
sort: Sort,
group: Group,
filter: Filter,

2
tests/playwright/pages/Dashboard/Command/CmdJPage.ts

@ -14,7 +14,7 @@ export class CmdJ extends BasePage {
}
async openCmdJ() {
await this.dashboardPage.rootPage.keyboard.press(this.isMacOs() ? 'Meta+J' : 'Control+J');
await this.dashboardPage.rootPage.keyboard.press((await this.isMacOs()) ? 'Meta+J' : 'Control+J');
// await this.dashboardPage.rootPage.waitForSelector('.DocSearch-Input');
}

3
tests/playwright/pages/Dashboard/Command/CmdKPage.ts

@ -14,8 +14,7 @@ export class CmdK extends BasePage {
}
async openCmdK() {
await this.dashboardPage.rootPage.keyboard.press(this.isMacOs() ? 'Meta+K' : 'Control+K');
// await this.dashboardPage.rootPage.waitForSelector('.DocSearch-Input');
await this.dashboardPage.rootPage.keyboard.press((await this.isMacOs()) ? 'Meta+K' : 'Control+K');
}
async searchText(text: string) {

9
tests/playwright/pages/Dashboard/Command/CmdLPage.ts

@ -1,5 +1,6 @@
import BasePage from '../../Base';
import { DashboardPage } from '..';
import { expect } from '@playwright/test';
export class CmdL extends BasePage {
readonly dashboardPage: DashboardPage;
@ -14,17 +15,15 @@ export class CmdL extends BasePage {
}
async openCmdL() {
await this.dashboardPage.rootPage.keyboard.press(this.isMacOs() ? 'Meta+L' : 'Control+L');
await this.dashboardPage.rootPage.keyboard.press((await this.isMacOs()) ? 'Meta+l' : 'Control+l');
}
async isCmdLVisible() {
const isVisible = this.get();
return await isVisible.count();
await expect(this.get()).toBeVisible();
}
async isCmdLNotVisible() {
const isNotVisible = this.get();
return await isNotVisible.count();
await expect(this.get()).toBeHidden();
}
async moveDown() {

98
tests/playwright/tests/db/features/command.spec.ts

@ -0,0 +1,98 @@
import { expect, test } from '@playwright/test';
import { DashboardPage } from '../../../pages/Dashboard';
import setup, { unsetup } from '../../../setup';
test.describe('Command Shortcuts', () => {
let dashboard: DashboardPage;
let context: any;
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: false, isSuperUser: true });
dashboard = new DashboardPage(page, context.base);
});
test.afterEach(async () => {
await unsetup(context);
});
test('Verify Command J Docs', async ({ page }) => {
await page.waitForTimeout(1000);
await dashboard.cmdJ.openCmdJ();
await expect(dashboard.cmdJ.get()).toBeVisible();
await dashboard.cmdJ.searchText('Column');
await page.keyboard.press('Escape');
await expect(dashboard.cmdJ.get()).toBeHidden();
await dashboard.signOut();
await page.waitForTimeout(2000);
await dashboard.cmdJ.openCmdJ();
await expect(dashboard.cmdJ.get()).toBeHidden();
});
test('Verify Command K', async ({ page }) => {
await page.waitForTimeout(1000);
await dashboard.cmdK.openCmdK();
await expect(dashboard.cmdK.get()).toBeVisible();
await page.keyboard.press('Escape');
await expect(dashboard.cmdK.get()).toBeHidden();
await dashboard.cmdK.openCmdK();
await dashboard.cmdK.searchText('CustomerList');
await expect(dashboard.get().locator('.nc-active-view-title')).toContainText('Default View');
await dashboard.signOut();
await page.waitForTimeout(1000);
await dashboard.cmdK.openCmdK();
await expect(dashboard.cmdK.get()).toBeHidden();
});
test('Verify Command L Recent Switch', async ({ page }) => {
await page.waitForTimeout(1000);
await dashboard.cmdL.openCmdL();
await dashboard.cmdL.isCmdLVisible();
await page.keyboard.press('Escape');
await dashboard.cmdL.isCmdLNotVisible();
await dashboard.treeView.openTable({ title: 'Actor' });
await dashboard.treeView.openTable({ title: 'Address' });
await dashboard.treeView.openTable({ title: 'Category' });
await dashboard.treeView.openTable({ title: 'City' });
await dashboard.treeView.openTable({ title: 'Country' });
await page.waitForTimeout(1000);
await dashboard.cmdL.openCmdL();
await page.waitForTimeout(1000);
await dashboard.cmdL.moveDown();
await dashboard.cmdL.moveDown();
await dashboard.cmdL.moveDown();
await dashboard.cmdL.openRecent();
await page.waitForTimeout(1000);
expect(await dashboard.cmdL.getActiveViewTitle()).toBe('Default View');
expect(await dashboard.cmdL.getActiveTableTitle()).toBe('Address');
await dashboard.signOut();
await dashboard.cmdL.openCmdL();
await dashboard.cmdL.isCmdLNotVisible();
});
});
Loading…
Cancel
Save