Browse Source

Merge branch 'develop' into fix/link

pull/6486/head
Sreehari jayaraj 1 year ago committed by GitHub
parent
commit
4306db4c12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  2. 12
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  3. 3
      packages/nc-gui/components/dlg/ProjectDelete.vue
  4. 4
      packages/nc-gui/components/dlg/TableDelete.vue
  5. 3
      packages/nc-gui/components/smartsheet/Cell.vue
  6. 2
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  7. 609
      packages/nc-gui/components/smartsheet/details/Fields.vue
  8. 11
      packages/nc-gui/store/views.ts
  9. 13
      packages/nc-gui/windi.config.ts
  10. 39
      tests/playwright/pages/Dashboard/Command/CmdJPage.ts
  11. 35
      tests/playwright/pages/Dashboard/Command/CmdKPage.ts
  12. 49
      tests/playwright/pages/Dashboard/Command/CmdLPage.ts
  13. 9
      tests/playwright/pages/Dashboard/index.ts

3
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -26,7 +26,8 @@ const logout = async () => {
try { try {
await signOut(false) await signOut(false)
await clearWorkspaces() // No need as all stores are cleared on signout
// await clearWorkspaces()
await navigateTo('/signin') await navigateTo('/signin')
} catch (e) { } catch (e) {

12
packages/nc-gui/components/dashboard/TreeView/ViewsList.vue

@ -45,7 +45,7 @@ const isDefaultBase = computed(() => {
return _isDefaultBase(base) return _isDefaultBase(base)
}) })
const { viewsByTable, activeView } = storeToRefs(useViewsStore()) const { viewsByTable, activeView, recentViews } = storeToRefs(useViewsStore())
const { navigateToTable } = useTablesStore() const { navigateToTable } = useTablesStore()
@ -57,7 +57,7 @@ const { refreshCommandPalette } = useCommandPalette()
const { addUndo, defineModelScope } = useUndoRedo() const { addUndo, defineModelScope } = useUndoRedo()
const { navigateToView, loadViews } = useViewsStore() const { navigateToView, loadViews, removeFromRecentViews } = useViewsStore()
/** Selected view(s) for menu */ /** Selected view(s) for menu */
const selected = ref<string[]>([]) const selected = ref<string[]>([])
@ -244,6 +244,13 @@ async function onRename(view: ViewType, originalTitle?: string, undo = false) {
scope: defineModelScope({ view: activeView.value }), scope: defineModelScope({ view: activeView.value }),
}) })
} }
// update view name in recent views
recentViews.value = recentViews.value.map((rv) => {
if (rv.viewId === view.id && rv.tableID === view.fk_model_id) {
rv.viewName = view.title
}
return rv
})
// View renamed successfully // View renamed successfully
// message.success(t('msg.success.viewRenamed')) // message.success(t('msg.success.viewRenamed'))
@ -265,6 +272,7 @@ function openDeleteDialog(view: ViewType) {
emits('deleted') emits('deleted')
removeFromRecentViews({ viewId: view.id, tableId: view.fk_model_id, projectId: project.value.id })
refreshCommandPalette() refreshCommandPalette()
if (activeView.value?.id === view.id) { if (activeView.value?.id === view.id) {
navigateToTable({ navigateToTable({

3
packages/nc-gui/components/dlg/ProjectDelete.vue

@ -14,6 +14,8 @@ const projectsStore = useProjects()
const { deleteProject, navigateToFirstProjectOrHome } = projectsStore const { deleteProject, navigateToFirstProjectOrHome } = projectsStore
const { projects } = storeToRefs(projectsStore) const { projects } = storeToRefs(projectsStore)
const { removeFromRecentViews } = useViewsStore()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const project = computed(() => projects.value.get(props.projectId)) const project = computed(() => projects.value.get(props.projectId))
@ -41,6 +43,7 @@ const onDelete = async () => {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} finally { } finally {
isLoading.value = false isLoading.value = false
removeFromRecentViews({ projectId: toBeDeletedProject.id! })
} }
} }
</script> </script>

4
packages/nc-gui/components/dlg/TableDelete.vue

@ -20,6 +20,7 @@ const { getMeta, removeMeta } = useMetas()
const { loadTables, projectUrl, isXcdbBase } = useProject() const { loadTables, projectUrl, isXcdbBase } = useProject()
const { refreshCommandPalette } = useCommandPalette() const { refreshCommandPalette } = useCommandPalette()
const { removeFromRecentViews } = useViewsStore()
const { projectTables, activeTable } = storeToRefs(useTablesStore()) const { projectTables, activeTable } = storeToRefs(useTablesStore())
const { openTable } = useTablesStore() const { openTable } = useTablesStore()
@ -69,6 +70,9 @@ const onDelete = async () => {
await loadTables() await loadTables()
// Remove from recent views
removeFromRecentViews({ projectId: props.projectId, tableId: toBeDeletedTable.id as string })
removeMeta(toBeDeletedTable.id as string) removeMeta(toBeDeletedTable.id as string)
refreshCommandPalette() refreshCommandPalette()
// Deleted table successfully // Deleted table successfully

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

@ -22,6 +22,7 @@ import {
isDate, isDate,
isDateTime, isDateTime,
isDecimal, isDecimal,
isDrawerExist,
isDuration, isDuration,
isEmail, isEmail,
isFloat, isFloat,
@ -207,7 +208,7 @@ onUnmounted(() => {
'h-10': isForm && !isSurveyForm && !isAttachment(column) && !props.virtual, 'h-10': isForm && !isSurveyForm && !isAttachment(column) && !props.virtual,
'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu, 'nc-grid-numeric-cell-left': (isForm && isNumericField && isExpandedFormOpen) || isEditColumnMenu,
'!min-h-30 resize-y': isTextArea(column) && (isForm || isSurveyForm), '!min-h-30 resize-y': isTextArea(column) && (isForm || isSurveyForm),
'!border-2 !border-brand-500': props.editEnabled && (isSurveyForm || isForm), '!border-2 !border-brand-500': props.editEnabled && (isSurveyForm || isForm) && !isDrawerExist(),
}, },
]" ]"
@keydown.enter.exact="navigate(NavigateDir.NEXT, $event)" @keydown.enter.exact="navigate(NavigateDir.NEXT, $event)"

2
packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -228,7 +228,7 @@ if (props.fromTableExplorer) {
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<a-form-item v-if="isFieldsTab" v-bind="validateInfos.title" class="flex flex-grow"> <a-form-item v-if="isFieldsTab" v-bind="validateInfos.title" class="flex flex-grow">
<div <div
class="flex flex-grow px-2 py-1 items-center rounded-lg bg-white hover:bg-gray-100 focus:bg-gray-100 outline-none" class="flex flex-grow px-2 py-1 items-center rounded-lg bg-gray-100 focus:bg-gray-100 outline-none"
style="outline-style: solid; outline-width: thin" style="outline-style: solid; outline-width: thin"
> >
<input <input

609
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -88,7 +88,7 @@ const getFieldOrder = (field?: TableExplorerColumn) => {
const fields = computed<TableExplorerColumn[]>({ const fields = computed<TableExplorerColumn[]>({
get: () => { get: () => {
const x = (meta.value?.columns as ColumnType[]) const x = ((meta.value?.columns as ColumnType[]) ?? [])
.filter((field) => !field.fk_column_id && !isSystemColumn(field)) .filter((field) => !field.fk_column_id && !isSystemColumn(field))
.concat(newFields.value) .concat(newFields.value)
.sort((a, b) => { .sort((a, b) => {
@ -252,13 +252,30 @@ const duplicateField = async (field: TableExplorerColumn) => {
const onFieldUpdate = (state: TableExplorerColumn) => { const onFieldUpdate = (state: TableExplorerColumn) => {
const col = fields.value.find((col) => compareCols(col, state)) const col = fields.value.find((col) => compareCols(col, state))
if (!col) return if (!col) return
const diffs = diff(col, state) const diffs = diff(col, state)
if (Object.keys(diffs).length === 0 || (Object.keys(diffs).length === 1 && 'altered' in diffs)) { if (Object.keys(diffs).length === 0 || (Object.keys(diffs).length === 1 && 'altered' in diffs)) {
ops.value = ops.value.filter((op) => op.op === 'add' || !compareCols(op.column, state)) ops.value = ops.value.filter((op) => op.op === 'add' || !compareCols(op.column, state))
} else { } else {
const field = ops.value.find((op) => compareCols(op.column, state)) const field = ops.value.find((op) => compareCols(op.column, state))
if (field) { const moveField = moveOps.value.find((op) => compareCols(op.column, state))
const isNewField = newFields.value.find((nField) => compareCols(nField, state))
if (isNewField) {
newFields.value = newFields.value.map((op) => {
if (compareCols(op, state)) {
ops.value = ops.value.filter((op) => op.op === 'add' && !compareCols(op.column, state))
ops.value.push({
op: 'add',
column: state,
})
return state
}
return op
})
return
}
if (field && !moveField) {
field.column = state field.column = state
} else { } else {
ops.value.push({ ops.value.push({
@ -326,11 +343,22 @@ const onFieldAdd = (state: TableExplorerColumn) => {
} }
const onMove = (_event: { moved: { newIndex: number; oldIndex: number } }) => { const onMove = (_event: { moved: { newIndex: number; oldIndex: number } }) => {
const order = calculateOrderForIndex(_event.moved.newIndex, _event.moved.newIndex < _event.moved.oldIndex)
const field = fields.value[_event.moved.oldIndex] const field = fields.value[_event.moved.oldIndex]
const order = calculateOrderForIndex(_event.moved.newIndex, _event.moved.newIndex < _event.moved.oldIndex)
const op = ops.value.find((op) => compareCols(op.column, field)) const op = ops.value.find((op) => compareCols(op.column, field))
if (op?.op === 'update') {
const diffs = diff(op.column, field)
if (!(Object.keys(diffs).length === 1 && 'column_order' in diffs)) {
message.warning('You cannot move field that is being edited. Either save or discard changes first')
return
}
}
if (op?.op === 'delete') {
message.warning('You cannot move field that is deleted. Either save or discard changes first')
return
}
if (op) { if (op) {
onFieldUpdate({ onFieldUpdate({
@ -364,6 +392,36 @@ const onMove = (_event: { moved: { newIndex: number; oldIndex: number } }) => {
} }
} }
const isColumnValid = (column: TableExplorerColumn) => {
const isDeleteOp = ops.value.find((op) => compareCols(column, op.column) && op.op === 'delete')
const isNew = ops.value.find((op) => compareCols(column, op.column) && op.op === 'add')
if (isDeleteOp) return true
if (!column.title) {
return false
}
if ((column.uidt === UITypes.Links || column.uidt === UITypes.LinkToAnotherRecord) && isNew) {
if (!column.childColumn || !column.childTable || !column.childId) {
return false
}
}
if (column.uidt === UITypes.Lookup && isNew) {
if (!column.fk_relation_column_id || !column.fk_lookup_column_id) {
return false
}
}
if (column.uidt === UITypes.Rollup && isNew) {
if (!column.fk_relation_column_id || !column.fk_rollup_column_id || !column.rollup_function) {
return false
}
}
if (column.uidt === UITypes.Formula && isNew) {
if (!column.formula_raw) {
return false
}
}
return true
}
const recoverField = (state: TableExplorerColumn) => { const recoverField = (state: TableExplorerColumn) => {
const field = ops.value.find((op) => compareCols(op.column, state)) const field = ops.value.find((op) => compareCols(op.column, state))
if (field) { if (field) {
@ -371,6 +429,7 @@ const recoverField = (state: TableExplorerColumn) => {
ops.value = ops.value.filter((op) => !compareCols(op.column, state)) ops.value = ops.value.filter((op) => !compareCols(op.column, state))
} else if (field.op === 'update') { } else if (field.op === 'update') {
ops.value = ops.value.filter((op) => !compareCols(op.column, state)) ops.value = ops.value.filter((op) => !compareCols(op.column, state))
moveOps.value = moveOps.value.filter((op) => !compareCols(op.column, state))
} }
activeField.value = null activeField.value = null
changeField(fields.value.filter((fiel) => fiel.id === state.id)[0]) changeField(fields.value.filter((fiel) => fiel.id === state.id)[0])
@ -447,17 +506,18 @@ const saveChanges = async () => {
} }
} }
const res = await $api.dbTableColumn.bulk(meta.value?.id, {
hash: columnsHash.value,
ops: ops.value,
})
for (const op of visibilityOps.value) { for (const op of visibilityOps.value) {
await toggleFieldVisibility(op.visible, { await toggleFieldVisibility(op.visible, {
...op.column, ...op.column,
show: op.visible, show: op.visible,
}) })
} }
const res = await $api.dbTableColumn.bulk(meta.value?.id, {
hash: columnsHash.value,
ops: ops.value,
})
await loadViewColumns() await loadViewColumns()
if (res) { if (res) {
@ -502,6 +562,8 @@ const toggleVisibility = async (checked: boolean, field: Field) => {
}) })
} }
const isColumnsValid = computed(() => fields.value.every((f) => isColumnValid(f)))
onMounted(async () => { onMounted(async () => {
if (!meta.value?.id) return if (!meta.value?.id) return
columnsHash.value = (await $api.dbTableColumn.hash(meta.value?.id)).hash columnsHash.value = (await $api.dbTableColumn.hash(meta.value?.id)).hash
@ -509,306 +571,257 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="flex flex-col items-center w-full p-4" style="height: calc(100vh - (var(--topbar-height) * 2))"> <div class="w-full p-4">
<div class="h-full max-w-250 w-full"> <div class="max-w-250 h-full w-full mx-auto">
<div class="flex flex-col h-full"> <div class="flex w-full justify-between py-2">
<div class="flex w-full justify-between py-2"> <a-input v-model:value="searchQuery" class="!h-8 !px-1 !rounded-lg !w-72" placeholder="Search field">
<div class="flex flex-1 items-center gap-2"> <template #prefix>
<h1 class="font-bold text-base">Fields</h1> <GeneralIcon icon="search" class="mx-1 h-3.5 w-3.5 text-gray-500 group-hover:text-black" />
<div class="flex bg-gray-100 items-center mb-1.5 rounded-lg px-2"> </template>
<LazyGeneralEmojiPicker :emoji="selectedView?.meta?.icon" readonly size="xsmall"> <template #suffix>
<template #default> <GeneralIcon
<GeneralViewIcon :meta="{ type: selectedView?.type }" class="min-w-4.5 text-lg flex" /> v-if="searchQuery.length > 0"
</template> icon="close"
</LazyGeneralEmojiPicker> class="mx-1 h-3.5 w-3.5 text-gray-500 group-hover:text-black"
@click="searchQuery = ''"
<span class="text-sm pl-1.25 text-gray-700"> />
{{ selectedView?.title }} </template>
</span> </a-input>
<div class="flex gap-2">
<NcButton type="secondary" size="small" class="mr-1" :disabled="loading" @click="addField()">
<div class="flex items-center gap-2">
<GeneralIcon icon="plus" class="h-3.5 mb-1 w-3.5" />
New field
</div> </div>
</div> </NcButton>
<div class="flex gap-2"> <NcButton
<NcButton type="secondary"
type="secondary" size="small"
size="small" :disabled="!loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1"
:disabled="!loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1" @click="clearChanges()"
@click="clearChanges()" >
> Reset
Reset </NcButton>
</NcButton> <NcButton
<NcButton type="primary"
type="primary" size="small"
size="small" :loading="loading"
:loading="loading" :disabled="isColumnsValid ? !loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1 : true"
:disabled="!loading && ops.length < 1 && moveOps.length < 1 && visibilityOps.length < 1" @click="saveChanges()"
@click="saveChanges()" >
> Save changes
Save changes </NcButton>
</NcButton>
</div>
</div> </div>
</div>
<div class="flex gap-x-4 overflow-y-auto"> <div class="flex flex-row rounded-lg border-1 border-gray-200">
<div class="flex flex-col flex-1 nc-scrollbar-md"> <div class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height">
<div class="flex w-full justify-between pb-2 pr-1"> <Draggable v-model="fields" item-key="id" @change="onMove($event)">
<a-input v-model:value="searchQuery" class="!h-8 !px-1 !rounded-lg !w-3/6" placeholder="Search field"> <template #item="{ element: field }">
<template #prefix> <div
<GeneralIcon icon="search" class="mx-1 h-3.5 w-3.5 text-gray-500 group-hover:text-black" /> v-if="field.title.toLowerCase().includes(searchQuery.toLowerCase()) && !field.pv"
</template> class="flex px-2 hover:bg-gray-100 first:rounded-t-lg border-b-1 last:rounded-b-none border-gray-200 pl-5 group"
<template #suffix> :class="` ${compareCols(field, activeField) ? 'selected' : ''}`"
<GeneralIcon @click="changeField(field, $event)"
v-if="searchQuery.length > 0" >
icon="close" <div class="flex items-center flex-1 py-2.5 gap-1 w-2/6">
class="mx-1 h-3.5 w-3.5 text-gray-500 group-hover:text-black" <component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-600 mr-1" />
@click="searchQuery = ''" <NcCheckbox
v-if="field.id && viewFieldsMap[field.id]"
:checked="
visibilityOps.find((op) => op.column.fk_column_id === field.id)?.visible ?? viewFieldsMap[field.id].show
"
@change="
(event) => {
toggleVisibility(event.target.checked, viewFieldsMap[field.id])
}
"
/>
<NcCheckbox v-else :disabled="true" class="opacity-0" :checked="true" />
<SmartsheetHeaderCellIcon
v-if="field"
:column-meta="fieldState(field) || field"
:class="{
'text-brand-500': compareCols(field, activeField),
}"
/> />
</template> <span
</a-input> :class="{
<NcButton type="secondary" size="small" :disabled="loading" @click="addField()"> 'text-brand-500': compareCols(field, activeField),
<div class="flex items-center gap-2"> }"
<GeneralIcon icon="plus" class="h-3.5 mb-1 w-3.5" /> class="truncate max-w-64"
New field >
{{ fieldState(field)?.title || field.title }}
</span>
</div> </div>
</NcButton> <div class="flex items-center justify-end gap-1">
</div> <div class="flex items-center">
<Draggable v-model="fields" item-key="id" @change="onMove($event)"> <NcBadge v-if="fieldStatus(field) === 'delete'" color="red" :border="false" class="bg-red-50 text-red-700">
<template #item="{ element: field }"> Deleted field
<div </NcBadge>
v-if="field.title && field.title.toLowerCase().includes(searchQuery.toLowerCase()) && !field.pv" <NcBadge
class="flex px-2 mr-1 border-x-1 bg-white border-t-1 hover:bg-gray-100 first:rounded-t-lg last:border-b-1 last:rounded-b-lg pl-5 group" v-else-if="fieldStatus(field) === 'add'"
:class="` ${compareCols(field, activeField) ? 'selected' : ''}`" color="orange"
@click="changeField(field, $event)" :border="false"
> class="bg-green-50 text-green-700"
<div class="flex items-center flex-1 py-2.5 gap-1 w-2/6">
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-600 mr-1" />
<NcCheckbox
v-if="field.id && viewFieldsMap[field.id]"
:checked="
visibilityOps.find((op) => op.column.fk_column_id === field.id)?.visible ?? viewFieldsMap[field.id].show
"
@change="
(event) => {
toggleVisibility(event.target.checked, viewFieldsMap[field.id])
}
"
/>
<NcCheckbox v-else :disabled="true" class="opacity-0" :checked="true" />
<SmartsheetHeaderCellIcon
v-if="field"
:column-meta="fieldState(field) || field"
:class="{
'text-brand-500': compareCols(field, activeField),
}"
/>
<span
:class="{
'text-brand-500': compareCols(field, activeField),
}"
class="truncate max-w-64"
> >
{{ fieldState(field)?.title || field.title }} New field
</span> </NcBadge>
</div>
<div class="flex items-center justify-end gap-1"> <NcBadge
<div class="flex items-center"> v-else-if="fieldStatus(field) === 'update'"
<NcBadge v-if="fieldStatus(field) === 'delete'" color="red" :border="false" class="bg-red-50 text-red-700"> color="orange"
Deleted field :border="false"
</NcBadge> class="bg-orange-50 text-orange-700"
<NcBadge
v-else-if="fieldStatus(field) === 'add'"
color="orange"
:border="false"
class="bg-green-50 text-green-700"
>
New field
</NcBadge>
<NcBadge
v-else-if="fieldStatus(field) === 'update'"
color="orange"
:border="false"
class="bg-orange-50 text-orange-700"
>
Updated field
</NcBadge>
</div>
<NcButton
v-if="fieldStatus(field) === 'delete' || fieldStatus(field) === 'update'"
type="secondary"
size="small"
class="no-action mr-2"
:disabled="loading"
@click="recoverField(field)"
> >
<div class="flex items-center text-xs gap-1"> Updated field
<GeneralIcon icon="reload" /> </NcBadge>
Restore <NcBadge
</div> v-if="!isColumnValid(field)"
</NcButton> color="yellow"
<a-dropdown v-else :trigger="['click']" overlay-class-name="nc-dropdown-table-explorer" @click.stop> :border="false"
<GeneralIcon icon="threeDotVertical" class="no-action opacity-0 group-hover:(opacity-100) text-gray-500" /> class="ml-1 bg-yellow-50 text-yellow-700"
<template #overlay>
<a-menu>
<a-menu-item key="table-explorer-duplicate" @click="duplicateField(field)">
<div class="nc-project-menu-item">
<Icon class="iconify text-gray-800" icon="lucide:copy" /><span>Duplicate</span>
</div>
</a-menu-item>
<a-menu-item v-if="!field.pv" key="table-explorer-insert-above" @click="addField(field, true)">
<div class="nc-project-menu-item">
<Icon class="iconify text-gray-800" icon="lucide:arrow-up" /><span>Insert above</span>
</div>
</a-menu-item>
<a-menu-item key="table-explorer-insert-below" @click="addField(field)">
<div class="nc-project-menu-item">
<Icon class="iconify text-gray-800" icon="lucide:arrow-down" /><span>Insert below</span>
</div>
</a-menu-item>
<a-menu-divider class="my-0" />
<a-menu-item key="table-explorer-delete" @click="onFieldDelete(field)">
<div class="nc-project-menu-item group text-red-500">
<GeneralIcon icon="delete" class="group-hover:text-accent" />
Delete
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<MdiChevronRight
class="text-brand-500 opacity-0"
:class="{
'opacity-100': compareCols(field, activeField),
}"
/>
</div>
</div>
</template>
<template v-if="displayColumn && displayColumn.title.toLowerCase().includes(searchQuery.toLowerCase())" #header>
<div
class="flex px-2 mr-1 border-x-1 bg-white border-t-1 hover:bg-gray-100 first:rounded-t-lg last:border-b-1 last:rounded-b-lg pl-5 group"
:class="` ${compareCols(displayColumn, activeField) ? 'selected' : ''}`"
@click="changeField(displayColumn, $event)"
>
<div class="flex items-center flex-1 py-2.5 gap-1 w-2/6">
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-200 mr-1" />
<NcCheckbox :disabled="true" :checked="true" />
<SmartsheetHeaderCellIcon
v-if="displayColumn"
:column-meta="fieldState(displayColumn) || displayColumn"
:class="{
'text-brand-500': compareCols(displayColumn, activeField),
}"
/>
<span
:class="{
'text-brand-500': compareCols(displayColumn, activeField),
}"
> >
{{ fieldState(displayColumn)?.title || displayColumn.title }} Incomplete configuration
</span> </NcBadge>
</div> </div>
<div class="flex items-center justify-end gap-1"> <NcButton
<div class="flex items-center"> v-if="fieldStatus(field) === 'delete' || fieldStatus(field) === 'update'"
<NcBadge type="secondary"
v-if="fieldStatus(displayColumn) === 'delete'" size="small"
color="red" class="no-action mr-2"
:border="false" :disabled="loading"
class="bg-red-50 text-red-700" @click="recoverField(field)"
> >
Deleted field <div class="flex items-center text-xs gap-1">
</NcBadge> <GeneralIcon icon="reload" />
Restore
<NcBadge
v-else-if="fieldStatus(displayColumn) === 'update'"
color="orange"
:border="false"
class="bg-orange-50 text-orange-700"
>
Updated field
</NcBadge>
</div> </div>
<NcButton </NcButton>
v-if="fieldStatus(displayColumn) === 'delete' || fieldStatus(displayColumn) === 'update'" <NcDropdown v-else :trigger="['click']" overlay-class-name="nc-dropdown-table-explorer" @click.stop>
type="secondary" <GeneralIcon icon="threeDotVertical" class="no-action opacity-0 group-hover:(opacity-100) text-gray-500" />
size="small"
class="no-action mr-2" <template #overlay>
:disabled="loading" <NcMenu>
@click="recoverField(displayColumn)" <NcMenuItem key="table-explorer-duplicate" @click="duplicateField(field)">
<Icon class="iconify text-gray-800" icon="lucide:copy" /><span>Duplicate</span>
</NcMenuItem>
<NcMenuItem v-if="!field.pv" key="table-explorer-insert-above" @click="addField(field, true)">
<Icon class="iconify text-gray-800" icon="lucide:arrow-up" /><span>Insert above</span>
</NcMenuItem>
<NcMenuItem key="table-explorer-insert-below" @click="addField(field)">
<Icon class="iconify text-gray-800" icon="lucide:arrow-down" /><span>Insert below</span>
</NcMenuItem>
<a-menu-divider class="my-1" />
<NcMenuItem key="table-explorer-delete" @click="onFieldDelete(field)">
<div class="text-red-500">
<GeneralIcon icon="delete" class="group-hover:text-accent" />
Delete
</div>
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<MdiChevronRight
class="text-brand-500 opacity-0"
:class="{
'opacity-100': compareCols(field, activeField),
}"
/>
</div>
</div>
</template>
<template v-if="displayColumn && displayColumn.title.toLowerCase().includes(searchQuery.toLowerCase())" #header>
<div
class="flex px-2 bg-white hover:bg-gray-100 border-b-1 border-gray-200 first:rounded-tl-lg last:border-b-1 pl-5 group"
:class="` ${compareCols(displayColumn, activeField) ? 'selected' : ''}`"
@click="changeField(displayColumn, $event)"
>
<div class="flex items-center flex-1 py-2.5 gap-1 w-2/6">
<component :is="iconMap.drag" class="cursor-move !h-3.75 text-gray-200 mr-1" />
<NcCheckbox :disabled="true" :checked="true" />
<SmartsheetHeaderCellIcon
v-if="displayColumn"
:column-meta="fieldState(displayColumn) || displayColumn"
:class="{
'text-brand-500': compareCols(displayColumn, activeField),
}"
/>
<span
:class="{
'text-brand-500': compareCols(displayColumn, activeField),
}"
>
{{ fieldState(displayColumn)?.title || displayColumn.title }}
</span>
</div>
<div class="flex items-center justify-end gap-1">
<div class="flex items-center">
<NcBadge
v-if="fieldStatus(displayColumn) === 'delete'"
color="red"
:border="false"
class="bg-red-50 text-red-700"
> >
<div class="flex items-center text-xs gap-1"> Deleted field
<GeneralIcon icon="reload" /> </NcBadge>
Restore
</div> <NcBadge
</NcButton> v-else-if="fieldStatus(displayColumn) === 'update'"
<a-dropdown v-else :trigger="['click']" overlay-class-name="nc-dropdown-table-explorer" @click.stop> color="orange"
<GeneralIcon icon="threeDotVertical" class="no-action opacity-0 group-hover:(opacity-100) text-gray-500" /> :border="false"
class="bg-orange-50 text-orange-700"
<template #overlay> >
<a-menu> Updated field
<a-menu-item key="table-explorer-duplicate" @click="duplicateField(displayColumn)"> </NcBadge>
<div class="nc-project-menu-item">
<Icon class="iconify text-gray-800" icon="lucide:copy" /><span>Duplicate</span>
</div>
</a-menu-item>
<a-menu-item v-if="!field.pv" key="table-explorer-insert-above" @click="addField(displayColumn, true)">
<div class="nc-project-menu-item">
<Icon class="iconify text-gray-800" icon="lucide:arrow-up" /><span>Insert above</span>
</div>
</a-menu-item>
<a-menu-item key="table-explorer-insert-below" @click="addField(displayColumn)">
<div class="nc-project-menu-item">
<Icon class="iconify text-gray-800" icon="lucide:arrow-down" /><span>Insert below</span>
</div>
</a-menu-item>
<a-menu-divider class="my-0" />
<a-menu-item key="table-explorer-delete" @click="onFieldDelete(displayColumn)">
<div class="nc-project-menu-item group text-red-500">
<GeneralIcon icon="delete" class="group-hover:text-accent" />
Delete
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<MdiChevronRight
class="text-brand-500 opacity-0"
:class="{
'opacity-100': compareCols(displayColumn, activeField),
}"
/>
</div> </div>
</div> <NcButton
</template> v-if="fieldStatus(displayColumn) === 'delete' || fieldStatus(displayColumn) === 'update'"
</Draggable> type="secondary"
</div> size="small"
<Transition v-if="!changingField" name="slide-fade"> class="no-action mr-2"
<div class="flex p-4 h-fit w-1/3 border-gray-200 border-1 rounded-xl"> :disabled="loading"
<SmartsheetColumnEditOrAddProvider @click="recoverField(displayColumn)"
v-if="activeField" >
class="w-full" <div class="flex items-center text-xs gap-1">
:column="activeField" <GeneralIcon icon="reload" />
:preload="fieldState(activeField)" Restore
:table-explorer-columns="fields" </div>
embed-mode </NcButton>
from-table-explorer <MdiChevronRight
@update="onFieldUpdate" class="text-brand-500 opacity-0"
@add="onFieldAdd" :class="{
/> 'opacity-100': compareCols(displayColumn, activeField),
<div v-else class="flex flex-col gap-6 w-full items-center"> }"
<img src="~assets/img/fieldPlaceholder.svg" class="!w-[18rem]" /> />
<div class="text-2xl text-gray-600 font-bold text-center">Select a field</div>
<div class="text-center text-sm px-2 text-gray-500">
Make changes to field properties by selecting a field from the list
</div> </div>
</div> </div>
</div> </template>
</Transition> </Draggable>
</div> </div>
<Transition v-if="!changingField" name="slide-fade">
<div class="border-gray-200 border-l-1 rounded-r-xl h-[calc(100vh-(var(--topbar-height)*3.85))]">
<SmartsheetColumnEditOrAddProvider
v-if="activeField"
class="p-4 w-[25rem]"
:column="activeField"
:preload="fieldState(activeField)"
:table-explorer-columns="fields"
embed-mode
from-table-explorer
@update="onFieldUpdate"
@add="onFieldAdd"
/>
<div v-else class="w-[25rem] flex flex-col justify-center p-4 items-center">
<img src="~assets/img/fieldPlaceholder.svg" class="!w-[18rem]" />
<div class="text-2xl text-gray-600 font-bold text-center pt-6">Select a field</div>
<div class="text-center text-sm px-2 text-gray-500 pt-6">
Make changes to field properties by selecting a field from the list
</div>
</div>
</div>
</Transition>
</div> </div>
</div> </div>
</div> </div>
@ -849,4 +862,8 @@ onMounted(async () => {
.slide-fade-leave-to { .slide-fade-leave-to {
opacity: 0; opacity: 0;
} }
.nc-fields-height {
height: calc(100vh - (var(--topbar-height) * 3.6));
}
</style> </style>

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

@ -1,4 +1,4 @@
import { type ViewType } from 'nocodb-sdk' import type { ViewType } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import type { ViewPageType } from '~/lib' import type { ViewPageType } from '~/lib'
@ -6,6 +6,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const router = useRouter() const router = useRouter()
const recentViews = ref<any>([])
const route = router.currentRoute const route = router.currentRoute
const tablesStore = useTablesStore() const tablesStore = useTablesStore()
@ -20,6 +21,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
viewsByTable.value.set(tablesStore.activeTableId, value) viewsByTable.value.set(tablesStore.activeTableId, value)
}, },
}) })
const isViewsLoading = ref(true) const isViewsLoading = ref(true)
const isViewDataLoading = ref(true) const isViewDataLoading = ref(true)
const isPublic = computed(() => route.value.meta?.public) const isPublic = computed(() => route.value.meta?.public)
@ -118,6 +120,10 @@ export const useViewsStore = defineStore('viewsStore', () => {
}) })
} }
const changeView = async (..._args: any) => {}
const removeFromRecentViews = (..._args: any) => {}
watch( watch(
() => tablesStore.activeTableId, () => tablesStore.activeTableId,
async (newId, oldId) => { async (newId, oldId) => {
@ -203,6 +209,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
isViewDataLoading, isViewDataLoading,
isPaginationLoading, isPaginationLoading,
loadViews, loadViews,
recentViews,
views, views,
activeView, activeView,
openedViewsTab, openedViewsTab,
@ -211,6 +218,8 @@ export const useViewsStore = defineStore('viewsStore', () => {
viewsByTable, viewsByTable,
activeViewTitleOrId, activeViewTitleOrId,
navigateToView, navigateToView,
changeView,
removeFromRecentViews,
} }
}) })

13
packages/nc-gui/windi.config.ts

@ -174,6 +174,19 @@ export default defineConfig({
800: '#164C66', 800: '#164C66',
900: '#0B2633', 900: '#0B2633',
}, },
yellow: {
50: '#fffbf2',
100: '#fff0d1',
200: '#fee5b0',
300: '#fdd889',
400: '#fdcb61',
500: '#fcbe3a',
600: '#ca982e',
700: '#977223',
800: '#654c17',
900: '#32260c',
},
primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))', primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))', accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))',
dark: colors.dark, dark: colors.dark,

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

@ -0,0 +1,39 @@
import BasePage from '../../Base';
import { DashboardPage } from '..';
export class CmdJ extends BasePage {
readonly dashboardPage: DashboardPage;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
this.dashboardPage = dashboard;
}
get() {
return this.dashboardPage.get().locator('.DocSearch');
}
async openCmdJ() {
await this.dashboardPage.rootPage.keyboard.press(this.isMacOs() ? 'Meta+J' : 'Control+J');
// await this.dashboardPage.rootPage.waitForSelector('.DocSearch-Input');
}
async searchText(text: string) {
await this.dashboardPage.rootPage.fill('.DocSearch-Input', text);
}
async isCmdJVisible() {
const isVisible = this.get();
return await isVisible.count();
}
async isCmdJNotVisible() {
const isNotVisible = this.get();
return await isNotVisible.count();
}
async getPlaceholderText() {
const placeholderText = this.get().locator('.DocSearch-Input');
return await placeholderText.innerText();
}
}

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

@ -0,0 +1,35 @@
import BasePage from '../../Base';
import { DashboardPage } from '..';
export class CmdK extends BasePage {
readonly dashboardPage: DashboardPage;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
this.dashboardPage = dashboard;
}
get() {
return this.dashboardPage.get().locator('.cmdk-modal.cmdk-modal-active');
}
async openCmdK() {
await this.dashboardPage.rootPage.keyboard.press(this.isMacOs() ? 'Meta+K' : 'Control+K');
// await this.dashboardPage.rootPage.waitForSelector('.DocSearch-Input');
}
async searchText(text: string) {
await this.dashboardPage.rootPage.fill('.cmdk-input', text);
await this.rootPage.keyboard.press('Enter');
}
async isCmdKVisible() {
const isVisible = this.get();
return await isVisible.count();
}
async isCmdKNotVisible() {
const isNotVisible = this.get();
return await isNotVisible.count();
}
}

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

@ -0,0 +1,49 @@
import BasePage from '../../Base';
import { DashboardPage } from '..';
export class CmdL extends BasePage {
readonly dashboardPage: DashboardPage;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
this.dashboardPage = dashboard;
}
get() {
return this.dashboardPage.get().locator('.cmdl-modal.cmdl-modal-active');
}
async openCmdL() {
await this.dashboardPage.rootPage.keyboard.press(this.isMacOs() ? 'Meta+L' : 'Control+L');
}
async isCmdLVisible() {
const isVisible = this.get();
return await isVisible.count();
}
async isCmdLNotVisible() {
const isNotVisible = this.get();
return await isNotVisible.count();
}
async moveDown() {
await this.dashboardPage.rootPage.keyboard.press('ArrowDown');
}
async moveUp() {
await this.dashboardPage.rootPage.keyboard.press('ArrowUp');
}
async openRecent() {
await this.dashboardPage.rootPage.keyboard.press('Enter');
}
async getActiveViewTitle() {
return await this.dashboardPage.get().locator('.nc-active-view-title').innerText();
}
async getActiveTableTitle() {
return await this.dashboardPage.get().locator('.nc-active-table-title').innerText();
}
}

9
tests/playwright/pages/Dashboard/index.ts

@ -25,6 +25,9 @@ import { ProjectTypes } from 'nocodb-sdk';
import { WorkspacePage } from '../WorkspacePage'; import { WorkspacePage } from '../WorkspacePage';
import { DetailsPage } from './Details'; import { DetailsPage } from './Details';
import { WorkspaceSettingsObject } from './WorkspaceSettings'; import { WorkspaceSettingsObject } from './WorkspaceSettings';
import { CmdJ } from './Command/CmdJPage';
import { CmdK } from './Command/CmdKPage';
import { CmdL } from './Command/CmdLPage';
export class DashboardPage extends BasePage { export class DashboardPage extends BasePage {
readonly project: any; readonly project: any;
@ -55,6 +58,9 @@ export class DashboardPage extends BasePage {
readonly shareProjectButton: ShareProjectButtonPage; readonly shareProjectButton: ShareProjectButtonPage;
readonly details: DetailsPage; readonly details: DetailsPage;
readonly workspaceSettings: WorkspaceSettingsObject; readonly workspaceSettings: WorkspaceSettingsObject;
readonly cmdJ: CmdJ;
readonly cmdK: CmdK;
readonly cmdL: CmdL;
constructor(rootPage: Page, project: any) { constructor(rootPage: Page, project: any) {
super(rootPage); super(rootPage);
@ -88,6 +94,9 @@ export class DashboardPage extends BasePage {
this.shareProjectButton = new ShareProjectButtonPage(this); this.shareProjectButton = new ShareProjectButtonPage(this);
this.details = new DetailsPage(this); this.details = new DetailsPage(this);
this.workspaceSettings = new WorkspaceSettingsObject(this); this.workspaceSettings = new WorkspaceSettingsObject(this);
this.cmdJ = new CmdJ(this);
this.cmdK = new CmdK(this);
this.cmdL = new CmdL(this);
} }
get() { get() {

Loading…
Cancel
Save