Browse Source

Nc Refactor: dialog modals (#8743)

* fix(nc-gui): update view create modal for grid & kanban

* fix(nc-gui): update calendar view create modal

* feat(nc-gui): add support to select gallery cover image while creating/duplicating view

* fix(nc-gui): update view icon size in view create modal

* fix(nc-gui): add input shadow effect

* refactor(nc-gui): dlg ui

* chore(nc-gui): lint

* fix(nc-gui): small changes

* fix(nc-gui): review changes

* chore: revert display message

* chore: lint

* fix(nc-gui): truncate field name

* fix(nc-gui): ignore empty lines at the end in form view #3104

---------

Co-authored-by: Raju Udava <86527202+dstala@users.noreply.github.com>
pull/8767/head
Ramesh Mane 2 weeks ago committed by GitHub
parent
commit
50f287f215
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 64
      packages/nc-gui/assets/style.scss
  2. 2
      packages/nc-gui/components/cell/RichText.vue
  3. 28
      packages/nc-gui/components/cell/attachment/RenameFile.vue
  4. 3
      packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue
  5. 4
      packages/nc-gui/components/dashboard/TreeView/ViewsList.vue
  6. 5
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  7. 2
      packages/nc-gui/components/dlg/ProjectDelete.vue
  8. 11
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  9. 60
      packages/nc-gui/components/dlg/TableCreate.vue
  10. 2
      packages/nc-gui/components/dlg/TableDelete.vue
  11. 11
      packages/nc-gui/components/dlg/TableDuplicate.vue
  12. 13
      packages/nc-gui/components/dlg/TableRename.vue
  13. 169
      packages/nc-gui/components/dlg/ViewCreate.vue
  14. 2
      packages/nc-gui/components/dlg/ViewDelete.vue
  15. 2
      packages/nc-gui/components/dlg/WorkspaceDelete.vue
  16. 7
      packages/nc-gui/components/general/DeleteModal.vue
  17. 2
      packages/nc-gui/components/nc/Select.vue
  18. 6
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  19. 23
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  20. 15
      packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue
  21. 1
      packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue
  22. 15
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  23. 3
      packages/nc-gui/lang/en.json
  24. 1
      packages/nc-gui/windi.config.ts
  25. 5
      packages/nocodb/src/models/GalleryView.ts
  26. 5
      packages/nocodb/src/models/KanbanView.ts

64
packages/nc-gui/assets/style.scss

@ -60,6 +60,55 @@ main {
@apply !rounded-lg !py-2 !px-3 mb-1;
}
.nc-input-sm {
@apply !rounded-lg !py-1 !px-3;
}
.nc-input-shadow {
&.ant-input {
&:not(:hover):not(:focus):not(:disabled) {
@apply shadow-default border-gray-200;
}
&:hover:not(:focus):not(:disabled) {
@apply border-gray-200 shadow-hover;
}
&:focus {
@apply shadow-selected ring-0;
}
}
}
.ant-form-item-explain {
@apply !min-h-5;
.ant-form-item-explain-error {
@apply text-sm;
&:first-child {
@apply mt-1;
}
}
}
.ant-form-item-has-error :not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-borderless).ant-input-affix-wrapper,
.ant-form-item-has-error
:not(.ant-input-affix-wrapper-disabled):not(.ant-input-affix-wrapper-borderless).ant-input-affix-wrapper:hover,
.ant-form-item-has-error :not(.ant-input-disabled):not(.ant-input-borderless).ant-input,
.ant-form-item-has-error :not(.ant-input-disabled):not(.ant-input-borderless).ant-input:hover,
.ant-form-item-has-error
:not(.ant-input-number-affix-wrapper-disabled):not(.ant-input-number-affix-wrapper-borderless).ant-input-number-affix-wrapper,
.ant-form-item-has-error
:not(.ant-input-number-affix-wrapper-disabled):not(
.ant-input-number-affix-wrapper-borderless
).ant-input-number-affix-wrapper:hover {
border-color: var(--ant-error-color) !important;
}
ant-form-item-explain-error {
&:first-child {
@apply mt-2;
}
}
.mobile {
.nc-scrollbar-md,
.nc-scrollbar-lg,
@ -266,6 +315,21 @@ a {
}
}
.ant-form-item:not(.ant-form-item-has-error)
.nc-select-shadow.ant-select-focused:not(.ant-select-disabled).ant-select:not(.ant-select-customize-input)
.ant-select-selector {
@apply !shadow-selected;
}
.ant-form-item.ant-form-item-has-error
.nc-select-shadow.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input).ant-select-focused
.ant-select-selector,
.ant-form-item.ant-form-item-has-error
.nc-select-shadow.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input).ant-select-open
.ant-select-selector {
@apply !shadow-error;
}
// select dropdown border style
.ant-select-dropdown {
@apply border-1 border-gray-200 rounded-lg;

2
packages/nc-gui/components/cell/RichText.vue

@ -217,7 +217,7 @@ const onFocusWrapper = () => {
if (props.syncValueChange) {
watch([vModel, editor], () => {
setEditorContent(vModel.value)
setEditorContent(isFormField.value ? (vModel.value || '')?.replace(/(<br \/>)+$/g, '') : vModel.value)
})
}

28
packages/nc-gui/components/cell/attachment/RenameFile.vue

@ -46,28 +46,24 @@ onMounted(() => {
</script>
<template>
<GeneralModal v-model:visible="visible" class="nc-attachment-rename-modal !w-[30rem]">
<div class="flex flex-col items-center justify-center h-full p-8">
<div class="text-lg font-semibold self-start mb-4">{{ $t('title.renameFile') }}</div>
<GeneralModal v-model:visible="visible" class="nc-attachment-rename-modal" size="small">
<div class="flex flex-col items-center justify-center h-full p-6">
<div class="text-lg font-semibold self-start mb-5">{{ $t('title.renameFile') }}</div>
<a-form class="w-full h-full" no-style :model="form" @finish="renameFile(form.title)">
<a-form-item class="w-full" name="title" :rules="rules.title">
<a-input ref="inputEl" v-model:value="form.title" class="w-full" :placeholder="$t('general.rename')" />
<a-form-item class="w-full !mb-0" name="title" :rules="rules.title">
<a-input
ref="inputEl"
v-model:value="form.title"
class="w-full nc-input-sm nc-input-shadow"
:placeholder="$t('general.rename')"
/>
</a-form-item>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" html-type="back" type="secondary">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" html-type="submit" type="primary">{{ $t('general.confirm') }}</NcButton>
<NcButton key="back" html-type="back" size="small" type="secondary">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" html-type="submit" size="small" type="primary">{{ $t('general.confirm') }}</NcButton>
</div>
</a-form>
</div>
</GeneralModal>
</template>
<style scoped lang="scss">
.nc-attachment-rename-modal {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid border-primary border-opacity-50 rounded;
}
}
</style>

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

@ -37,6 +37,7 @@ async function onOpenModal({
copyViewId,
groupingFieldColumnId,
calendarRange,
coverImageColumnId,
}: {
title?: string
type: ViewTypes
@ -46,6 +47,7 @@ async function onOpenModal({
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
coverImageColumnId?: string
}) {
if (isViewListLoading.value) return
@ -69,6 +71,7 @@ async function onOpenModal({
'selectedViewId': copyViewId,
calendarRange,
groupingFieldColumnId,
coverImageColumnId,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()

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

@ -13,6 +13,7 @@ interface Emits {
title?: string
copyViewId?: string
groupingFieldColumnId?: string
coverImageColumnId?: string
},
): void
@ -337,6 +338,7 @@ function onOpenModal({
copyViewId,
groupingFieldColumnId,
calendarRange,
coverImageColumnId,
}: {
title?: string
type: ViewTypes
@ -346,6 +348,7 @@ function onOpenModal({
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
coverImageColumnId?: string
}) {
const isOpen = ref(true)
@ -358,6 +361,7 @@ function onOpenModal({
groupingFieldColumnId,
'views': views,
calendarRange,
coverImageColumnId,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()

5
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -21,7 +21,10 @@ interface Emits {
(event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
(
event: 'openModal',
data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string; coverImageColumnId?: string },
): void
}
const props = defineProps<Props>()

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

@ -51,7 +51,7 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700">
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon w-6 h-6 mx-1" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"

11
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -128,18 +128,17 @@ const isEaster = ref(false)
<GeneralModal
v-if="base"
v-model:visible="dialogShow"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
class="!w-[30rem]"
wrap-class-name="nc-modal-base-duplicate"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
<div class="font-medium text-lg text-gray-800 self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.project') }}
</div>
<div class="mt-4">{{ $t('msg.warning.duplicateProject') }}</div>
<div class="mt-5">{{ $t('msg.warning.duplicateProject') }}</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
@ -154,8 +153,10 @@ const isEaster = ref(false)
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:base:duplicate']" :loading="isLoading" @click="_duplicate"
<NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">{{
$t('general.cancel')
}}</NcButton>
<NcButton key="submit" v-e="['a:base:duplicate']" size="small" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>

60
packages/nc-gui/components/dlg/TableCreate.vue

@ -121,29 +121,46 @@ onMounted(() => {
</script>
<template>
<NcModal v-model:visible="dialogShow" :header="$t('activity.createTable')" size="small" @keydown.esc="dialogShow = false">
<NcModal
v-model:visible="dialogShow"
:show-separator="false"
:header="$t('activity.createTable')"
size="small"
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="table" class="!text-gray-600/75" />
<div class="flex flex-row items-center gap-x-2 text-base text-gray-800">
<GeneralIcon icon="table" class="!text-gray-600 w-5 h-5" />
{{ $t('activity.createTable') }}
</div>
</template>
<div class="flex flex-col mt-2">
<a-form :model="table" name="create-new-table-form" @keydown.enter="_createTable" @keydown.esc="dialogShow = false">
<a-form-item v-bind="validateInfos.title" :class="{ '!mb-1': isSnowflake(props.sourceId) }">
<a-input
ref="inputEl"
v-model:value="table.title"
class="nc-input-md"
hide-details
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>
<template v-if="isSnowflake(props.sourceId)">
<a-checkbox v-model:checked="table.is_hybrid" class="!flex flex-row items-center"> Hybrid Table </a-checkbox>
</template>
<div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<div class="flex flex-col mt-1">
<a-form
:model="table"
name="create-new-table-form"
class="flex flex-col gap-5"
@keydown.enter="_createTable"
@keydown.esc="dialogShow = false"
>
<div>
<a-form-item
v-bind="validateInfos.title"
:class="{ '!mb-1': isSnowflake(props.sourceId), '!mb-0': !isSnowflake(props.sourceId) }"
>
<a-input
ref="inputEl"
v-model:value="table.title"
class="nc-input-sm nc-input-shadow"
hide-details
data-testid="create-table-title-input"
:placeholder="$t('msg.info.enterTableName')"
/>
</a-form-item>
<template v-if="isSnowflake(props.sourceId)">
<a-checkbox v-model:checked="table.is_hybrid" class="!flex flex-row items-center"> Hybrid Table </a-checkbox>
</template>
</div>
<div v-if="isAdvanceOptVisible" class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<div>
<div class="mb-1">
<!-- Add Default Columns -->
@ -171,12 +188,13 @@ onMounted(() => {
</a-row>
</div>
</div>
<div class="flex flex-row justify-end gap-x-2 mt-2">
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<div class="flex flex-row justify-end gap-x-2">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
v-e="['a:table:create']"
type="primary"
size="small"
:disabled="validateInfos.title.validateStatus === 'error'"
:loading="creating"
@click="_createTable"

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

@ -110,7 +110,7 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.table')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="table" class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700">
<GeneralTableIcon :meta="table" class="nc-view-icon" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"

11
packages/nc-gui/components/dlg/TableDuplicate.vue

@ -129,7 +129,6 @@ const isEaster = ref(false)
<GeneralModal
v-model:visible="dialogShow"
:class="{ active: dialogShow }"
:closable="!isLoading"
:mask-closable="!isLoading"
:keyboard="!isLoading"
centered
@ -139,11 +138,11 @@ const isEaster = ref(false)
@keydown.esc="dialogShow = false"
>
<div>
<div class="prose-xl font-bold self-center" @dblclick="isEaster = !isEaster">
<div class="font-medium text-lg text-gray-800 self-center" @dblclick="isEaster = !isEaster">
{{ $t('general.duplicate') }} {{ $t('objects.table') }}
</div>
<div class="mt-4">{{ $t('msg.warning.duplicateTable') }}</div>
<div class="mt-5">{{ $t('msg.warning.duplicateTable') }}</div>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
@ -158,8 +157,10 @@ const isEaster = ref(false)
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton v-if="!isLoading" key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton key="submit" v-e="['a:table:duplicate']" type="primary" :loading="isLoading" @click="_duplicate"
<NcButton v-if="!isLoading" key="back" type="secondary" size="small" @click="dialogShow = false">{{
$t('general.cancel')
}}</NcButton>
<NcButton key="submit" v-e="['a:table:duplicate']" type="primary" size="small" :loading="isLoading" @click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>

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

@ -174,33 +174,34 @@ const renameTable = async (undo = false, disableTitleDiffCheck?: boolean | undef
</script>
<template>
<NcModal v-model:visible="dialogShow" size="small">
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false">
<template #header>
<div class="flex flex-row items-center gap-x-2">
<GeneralIcon icon="rename" />
{{ $t('activity.renameTable') }}
</div>
</template>
<div class="mt-2">
<div class="mt-1">
<a-form :model="formState" name="create-new-table-form">
<a-form-item v-bind="validateInfos.title">
<a-input
ref="inputEl"
v-model:value="formState.title"
class="nc-input-md"
class="nc-input-sm nc-input-shadow"
hide-details
size="large"
size="small"
:placeholder="$t('msg.info.enterTableName')"
@keydown.enter="() => renameTable()"
/>
</a-form-item>
</a-form>
<div class="flex flex-row justify-end gap-x-2 mt-6">
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
type="primary"
size="small"
:disabled="validateInfos.title.validateStatus === 'error' || formState.title?.trim() === tableMeta.title"
label="Rename Table"
loading-label="Renaming Table"

169
packages/nc-gui/components/dlg/ViewCreate.vue

@ -17,6 +17,7 @@ interface Props {
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
coverImageColumnId?: string
}
interface Emits {
@ -38,6 +39,7 @@ interface Form {
fk_from_column_id: string
fk_to_column_id: string | null // for ee only
}>
fk_cover_image_col_id: string | null
}
const props = withDefaults(defineProps<Props>(), {
@ -45,6 +47,7 @@ const props = withDefaults(defineProps<Props>(), {
groupingFieldColumnId: undefined,
geoDataFieldColumnId: undefined,
calendarRange: undefined,
coverImageColumnId: undefined,
})
const emits = defineEmits<Emits>()
@ -55,7 +58,7 @@ const { viewsByTable } = storeToRefs(useViewsStore())
const { refreshCommandPalette } = useCommandPalette()
const { selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId } = toRefs(props)
const { selectedViewId, groupingFieldColumnId, geoDataFieldColumnId, tableId, coverImageColumnId } = toRefs(props)
const meta = ref<TableType | undefined>()
@ -88,13 +91,14 @@ const form = reactive<Form>({
fk_grp_col_id: null,
fk_geo_data_col_id: null,
calendar_range: props.calendarRange || [],
fk_cover_image_col_id: null,
})
const viewSelectFieldOptions = ref<SelectProps['options']>([])
const viewNameRules = [
// name is required
{ required: true, message: `${t('labels.viewName')} ${t('general.required')}` },
{ required: true, message: `${t('labels.viewName')} ${t('general.required').toLowerCase()}` },
// name is unique
{
validator: (_: unknown, v: string) =>
@ -231,7 +235,7 @@ const addCalendarRange = async () => {
const isMetaLoading = ref(false)
onMounted(async () => {
if (props.type === ViewTypes.KANBAN || props.type === ViewTypes.MAP || props.type === ViewTypes.CALENDAR) {
if ([ViewTypes.GALLERY, ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(props.type)) {
isMetaLoading.value = true
try {
meta.value = (await getMeta(tableId.value))!
@ -258,6 +262,30 @@ onMounted(async () => {
}
}
// preset the cover image field
if (props.type === ViewTypes.GALLERY) {
viewSelectFieldOptions.value = [
{ value: null, label: 'No Image' },
...meta.value
.columns!.filter((el) => el.uidt === UITypes.Attachment)
.map((field) => {
return {
value: field.id,
label: field.title,
uidt: field.uidt,
}
}),
]
if (coverImageColumnId.value) {
form.fk_cover_image_col_id = coverImageColumnId.value
} else if (viewSelectFieldOptions.value.length > 1 && !form.copy_from_id) {
form.fk_cover_image_col_id = viewSelectFieldOptions.value[1].value as string
} else {
form.fk_cover_image_col_id = null
}
}
// preset the grouping field column
if (props.type === ViewTypes.KANBAN) {
viewSelectFieldOptions.value = meta.value
@ -266,6 +294,7 @@ onMounted(async () => {
return {
value: field.id,
label: field.title,
uidt: field.uidt,
}
})
@ -279,6 +308,14 @@ onMounted(async () => {
// if there is no grouping field column, disable the create button
isNecessaryColumnsPresent.value = false
}
if (coverImageColumnId.value) {
form.fk_cover_image_col_id = coverImageColumnId.value
} else if (viewSelectFieldOptions.value.length > 1 && !form.copy_from_id) {
form.fk_cover_image_col_id = viewSelectFieldOptions.value[1].value as string
} else {
form.fk_cover_image_col_id = null
}
}
if (props.type === ViewTypes.CALENDAR) {
@ -319,12 +356,14 @@ onMounted(async () => {
<template>
<NcModal
v-model:visible="vModel"
:size="[ViewTypes.KANBAN, ViewTypes.MAP, ViewTypes.CALENDAR].includes(form.type) ? 'medium' : 'small'"
class="nc-view-create-modal"
:show-separator="false"
:size="[ViewTypes.MAP].includes(form.type) ? 'medium' : 'small'"
>
<template #header>
<div class="flex w-full flex-row justify-between items-center">
<div class="flex font-bold text-base gap-x-3 items-center">
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-xl" />
<GeneralViewIcon :meta="{ type: form.type }" class="nc-view-icon !text-[24px] !leading-6 max-h-6 max-w-6" />
<template v-if="form.type === ViewTypes.GRID">
<template v-if="form.copy_from_id">
{{ $t('labels.duplicateGridView') }}
@ -380,24 +419,60 @@ onMounted(async () => {
:href="`https://docs.nocodb.com/views/view-types/${typeAlias}`"
target="_blank"
>
Go to Docs
Docs
</a>
</div>
</template>
<div class="mt-2">
<a-form v-if="isNecessaryColumnsPresent" ref="formValidator" :model="form" layout="vertical">
<div class="mt-1">
<a-form v-if="isNecessaryColumnsPresent" ref="formValidator" :model="form" layout="vertical" class="flex flex-col gap-y-5">
<a-form-item :rules="viewNameRules" name="title">
<a-input
ref="inputEl"
v-model:value="form.title"
:placeholder="$t('labels.viewName')"
autofocus
class="nc-input-md h-10"
class="nc-input-sm nc-input-shadow"
@keydown.enter="onSubmit"
/>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.KANBAN"
v-if="form.type === ViewTypes.GALLERY && !form.copy_from_id"
:label="`${$t('labels.coverImageField')}`"
name="fk_cover_image_col_id"
>
<NcSelect
v-model:value="form.fk_cover_image_col_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
dropdown-match-select-width
:not-found-content="$t('placeholder.selectGroupFieldNotFound')"
:placeholder="$t('placeholder.selectCoverImageField')"
class="nc-select-shadow w-full nc-gallery-cover-image-field-select"
>
<a-select-option v-for="option of viewSelectFieldOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between" :title="option.label">
<div class="flex-1 flex items-center gap-1 max-w-[calc(100%_-_24px)]">
<SmartsheetHeaderIcon v-if="option.value" :column="option" class="!ml-0" />
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="form.fk_cover_image_col_id === option.value"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.KANBAN && !form.copy_from_id"
:label="$t('general.groupingField')"
:rules="groupingFieldColumnRules"
name="fk_grp_col_id"
@ -406,11 +481,32 @@ onMounted(async () => {
v-model:value="form.fk_grp_col_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
dropdown-match-select-width
:not-found-content="$t('placeholder.selectGroupFieldNotFound')"
:options="viewSelectFieldOptions"
:placeholder="$t('placeholder.selectGroupField')"
class="w-full nc-kanban-grouping-field-select"
/>
class="nc-select-shadow w-full nc-kanban-grouping-field-select"
>
<a-select-option v-for="option of viewSelectFieldOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between" :title="option.label">
<div class="flex-1 flex items-center gap-1 max-w-[calc(100%_-_24px)]">
<SmartsheetHeaderIcon :column="option" class="!ml-0" />
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="form.fk_grp_col_id === option.value"
id="nc-selected-item-icon"
icon="check"
class="flex-none text-primary w-4 h-4"
/>
</div>
</a-select-option>
</NcSelect>
</a-form-item>
<a-form-item
v-if="form.type === ViewTypes.MAP"
@ -425,19 +521,19 @@ onMounted(async () => {
:not-found-content="$t('placeholder.selectGeoFieldNotFound')"
:options="viewSelectFieldOptions"
:placeholder="$t('placeholder.selectGeoField')"
class="w-full"
class="nc-select-shadow w-full"
/>
</a-form-item>
<template v-if="form.type === ViewTypes.CALENDAR">
<div v-for="(range, index) in form.calendar_range" :key="`range-${index}`" class="flex w-full mb-2 items-center gap-2">
<span>
<template v-if="form.type === ViewTypes.CALENDAR && !form.copy_from_id">
<div v-for="(range, index) in form.calendar_range" :key="`range-${index}`" class="flex w-full items-center gap-2">
<span class="text-gray-800">
{{ $t('labels.organiseBy') }}
</span>
<NcSelect
v-model:value="range.fk_from_column_id"
:disabled="isMetaLoading"
:loading="isMetaLoading"
class="nc-from-select"
class="nc-select-shadow nc-from-select"
>
<a-select-option
v-for="(option, id) in [...viewSelectFieldOptions!].filter((f) => {
@ -453,7 +549,7 @@ onMounted(async () => {
>
<div class="flex w-full gap-2 justify-between items-center">
<div class="flex gap-2 items-center">
<SmartsheetHeaderIcon :column="option" />
<SmartsheetHeaderIcon :column="option" class="!ml-0" />
<NcTooltip class="truncate flex-1 max-w-18" placement="top" show-on-truncate-only>
<template #title>{{ option.label }}</template>
{{ option.label }}
@ -538,16 +634,16 @@ onMounted(async () => {
</a-form>
<div v-else-if="!isNecessaryColumnsPresent" class="flex flex-row p-4 border-gray-200 border-1 gap-x-4 rounded-lg w-full">
<div class="text-gray-500 flex gap-4">
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="warning" />
<GeneralIcon class="min-w-6 h-6 text-orange-500" icon="alertTriangle" />
<div class="flex flex-col gap-1">
<h2 class="font-semibold text-sm mb-0 text-gray-800">Suitable fields not present</h2>
<span class="text-gray-500 font-default"> {{ errorMessages[form.type] }}</span>
<span class="text-gray-500 font-default text-sm"> {{ errorMessages[form.type] }}</span>
</div>
</div>
</div>
<div class="flex flex-row w-full justify-end gap-x-2 mt-7">
<NcButton type="secondary" @click="vModel = false">
<div class="flex flex-row w-full justify-end gap-x-2 mt-5">
<NcButton type="secondary" size="small" @click="vModel = false">
{{ $t('general.cancel') }}
</NcButton>
@ -556,6 +652,7 @@ onMounted(async () => {
:disabled="!isNecessaryColumnsPresent"
:loading="isViewCreating"
type="primary"
size="small"
@click="onSubmit"
>
{{ $t('labels.createView') }}
@ -582,11 +679,29 @@ onMounted(async () => {
@apply !rounded-r-none;
}
.ant-input {
@apply border-gray-200;
.ant-form-item {
@apply !mb-0;
}
.ant-form-item {
@apply !mb-6;
.nc-input-sm {
@apply !mb-0;
}
.nc-view-create-modal {
:deep(.nc-modal) {
}
}
:deep(.ant-form-item-label > label) {
@apply !text-sm text-gray-800 flex;
&.ant-form-item-required:not(.ant-form-item-required-mark-optional)::before {
@apply content-[''] m-0;
}
}
:deep(.ant-select) {
.ant-select-selector {
@apply !rounded-lg;
}
}
</style>

2
packages/nc-gui/components/dlg/ViewDelete.vue

@ -46,7 +46,7 @@ async function onDelete() {
<template>
<GeneralDeleteModal v-model:visible="vModel" :entity-name="$t('objects.view')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="view" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="view" class="flex flex-row items-center py-2 px-3 bg-gray-50 rounded-lg text-gray-700">
<GeneralViewIcon :meta="props.view" class="nc-view-icon w-4 min-h-4"></GeneralViewIcon>
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-3"

2
packages/nc-gui/components/dlg/WorkspaceDelete.vue

@ -50,7 +50,7 @@ const onDelete = async () => {
<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.workspace')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="workspace" class="flex flex-row items-center py-2.25 px-2.75 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div v-if="workspace" class="flex flex-row items-center py-2.25 px-2.75 bg-gray-50 rounded-lg text-gray-700">
<GeneralIcon icon="workspace" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-2.25"

7
packages/nc-gui/components/general/DeleteModal.vue

@ -45,9 +45,7 @@ onKeyStroke('Enter', () => {
<template>
<GeneralModal v-model:visible="visible" size="small" centered>
<div class="flex flex-col p-6">
<div class="flex flex-row pb-2 mb-4 font-medium text-lg border-b-1 border-gray-50 text-gray-800">
{{ deleteLabel }} {{ props.entityName }}
</div>
<div class="flex flex-row pb-2 mb-3 font-medium text-lg text-gray-800">{{ deleteLabel }} {{ props.entityName }}</div>
<div class="mb-3 text-gray-800">
{{
@ -60,13 +58,14 @@ onKeyStroke('Enter', () => {
<slot name="entity-preview"></slot>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton type="secondary" @click="visible = false">
<NcButton type="secondary" size="small" @click="visible = false">
{{ $t('general.cancel') }}
</NcButton>
<NcButton
key="submit"
type="danger"
size="small"
html-type="submit"
:loading="isLoading"
data-testid="nc-delete-modal-delete-btn"

2
packages/nc-gui/components/nc/Select.vue

@ -84,7 +84,7 @@ const onChange = (value: string) => {
height: fit-content;
.ant-select-selector {
box-shadow: 0px 5px 3px -2px rgba(0, 0, 0, 0.02), 0px 3px 1px -2px rgba(0, 0, 0, 0.06);
@apply border-1 border-gray-200 rounded-lg;
@apply border-1 border-gray-200 rounded-lg shadow-default;
}
.ant-select-selection-item {

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

@ -959,7 +959,7 @@ export default {
<GeneralDeleteModal v-model:visible="showDeleteRowModal" entity-name="Record" :on-delete="() => onConfirmDeleteRowClick()">
<template #entity-preview>
<span>
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700">
<div class="text-ellipsis overflow-hidden select-none w-full pl-1.75 break-keep whitespace-nowrap">
<LazySmartsheetPlainCell v-model="displayValue" :column="displayField" />
</div>
@ -978,9 +978,9 @@ export default {
{{ $t('activity.doYouWantToSaveTheChanges') }}
</div>
<div class="flex flex-row justify-end gap-x-2 mt-5">
<NcButton type="secondary" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton>
<NcButton type="secondary" size="small" @click="discardPreventModal">{{ $t('labels.discard') }}</NcButton>
<NcButton key="submit" type="primary" :loading="isSaving" @click="saveChanges">
<NcButton key="submit" type="primary" size="small" :loading="isSaving" @click="saveChanges">
{{ $t('tooltip.saveChanges') }}
</NcButton>
</div>

23
packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue

@ -422,27 +422,38 @@ useMenuCloseOnEsc(open)
<div class="pl-2 flex text-sm select-none text-gray-600">{{ $t('labels.coverImageField') }}</div>
<div
class="flex-1 nc-dropdown-cover-image-wrapper flex items-stretch border-1 border-gray-200 rounded-lg transition-all duration-0.3s"
class="flex-1 nc-dropdown-cover-image-wrapper flex items-stretch border-1 border-gray-200 rounded-lg transition-all duration-0.3s max-w-[206px]"
>
<a-select
v-model:value="coverImageColumnId"
class="w-full"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg"
class="flex-1 max-w-[calc(100%_-_33px)]"
dropdown-class-name="nc-dropdown-cover-image !rounded-lg "
:bordered="false"
@click.stop
>
<template #suffixIcon><GeneralIcon class="text-gray-700" icon="arrowDown" /></template>
<a-select-option v-for="option of coverOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between">
<div class="flex items-center gap-1">
<div class="w-full flex gap-2 items-center justify-between max-w-[400px]">
<div
class="flex-1 flex items-center gap-1"
:class="{
'max-w-[calc(100%_-_20px)]': coverImageColumnId === option.value,
'max-w-full': coverImageColumnId !== option.value,
}"
>
<component
:is="getIcon(metaColumnById[option.value])"
v-if="option.value"
class="!w-3.5 !h-3.5 !text-gray-700 !ml-0"
/>
<span> {{ option.label }} </span>
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="coverImageColumnId === option.value"

15
packages/nc-gui/components/smartsheet/toolbar/StackedBy.vue

@ -146,9 +146,9 @@ const getIcon = (c: ColumnType) =>
{{ $t('activity.kanban.stackedBy') }}
</span>
<div
class="flex items-center rounded-md transition-colors duration-0.3s bg-gray-100 group-hover:bg-gray-200 px-1 min-h-5 text-gray-600"
class="flex items-center rounded-md transition-colors duration-0.3s bg-gray-100 group-hover:bg-gray-200 px-1 min-h-5 text-gray-600 max-w-[108px]"
>
<span class="font-weight-500 text-sm">{{ groupingField }}</span>
<span class="font-weight-500 text-sm truncate !leading-5">{{ groupingField }}</span>
</div>
</div>
</div>
@ -158,7 +158,7 @@ const getIcon = (c: ColumnType) =>
<div v-if="open" class="p-4 w-90 bg-white nc-table-toolbar-menu rounded-lg flex flex-col gap-5" @click.stop>
<div class="flex flex-col gap-2">
<div>
{{ $t('general.groupingField').toLowerCase().replace(/^./, $t('general.groupingField').charAt(0).toUpperCase()) }}
{{ $t('general.groupingField') }}
</div>
<div class="nc-fields-list">
<div class="grouping-field">
@ -173,14 +173,19 @@ const getIcon = (c: ColumnType) =>
<template #suffixIcon><GeneralIcon icon="arrowDown" class="text-gray-700" /></template>
<a-select-option v-for="option of singleSelectFieldOptions" :key="option.value" :value="option.value">
<div class="w-full flex gap-2 items-center justify-between" :title="option.label">
<div class="flex items-center gap-1">
<div class="flex items-center gap-1 max-w-[calc(100%_-_20px)]">
<component
:is="getIcon(metaColumnById[option.value])"
v-if="option.value"
class="!w-3.5 !h-3.5 !text-gray-700 !ml-0"
/>
<span> {{ option.label }} </span>
<NcTooltip class="flex-1 max-w-[calc(100%_-_20px)] truncate" show-on-truncate-only>
<template #title>
{{ option.label }}
</template>
<template #default>{{ option.label }}</template>
</NcTooltip>
</div>
<GeneralIcon
v-if="groupingFieldColumnId === option.value"

1
packages/nc-gui/components/smartsheet/toolbar/ViewActionMenu.vue

@ -103,6 +103,7 @@ function onDuplicate() {
'groupingFieldColumnId': view.value!.view!.fk_grp_col_id,
'views': views,
'calendarRange': view.value!.view!.calendar_range,
'coverImageColumnId': view.value!.view!.fk_cover_image_col_id,
'onUpdate:modelValue': closeDialog,
'onCreated': async (view: ViewType) => {
closeDialog()

15
packages/nc-gui/components/workspace/CreateProjectDlg.vue

@ -106,10 +106,10 @@ const typeLabel = computed(() => {
</script>
<template>
<NcModal v-model:visible="dialogShow" size="small">
<NcModal v-model:visible="dialogShow" size="small" :show-separator="false">
<template #header>
<!-- Create A New Table -->
<div class="flex flex-row items-center">
<div class="flex flex-row items-center text-base text-gray-800">
<GeneralProjectIcon :color="formState.meta.iconColor" :type="baseType" class="mr-2.5 !text-lg !h-4" />
{{
$t('general.createEntity', {
@ -118,7 +118,7 @@ const typeLabel = computed(() => {
}}
</div>
</template>
<div class="mt-3">
<div class="mt-1">
<a-form
ref="form"
:model="formState"
@ -129,24 +129,25 @@ const typeLabel = computed(() => {
autocomplete="off"
@finish="createProject"
>
<a-form-item name="title" :rules="nameValidationRules" class="m-10">
<a-form-item name="title" :rules="nameValidationRules" class="!mb-0">
<a-input
ref="input"
v-model:value="formState.title"
name="title"
class="nc-metadb-base-name nc-input-md"
class="nc-metadb-base-name nc-input-sm nc-input-shadow"
placeholder="Title"
/>
</a-form-item>
</a-form>
<div class="flex flex-row justify-end mt-7 gap-x-2">
<NcButton type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<div class="flex flex-row justify-end mt-5 gap-x-2">
<NcButton type="secondary" size="small" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
v-e="['a:base:create']"
data-testid="docs-create-proj-dlg-create-btn"
:loading="creating"
type="primary"
size="small"
:label="`${$t('general.create')} ${typeLabel}`"
:loading-label="`${$t('general.creating')} ${typeLabel}`"
@click="createProject"

3
packages/nc-gui/lang/en.json

@ -157,7 +157,7 @@
"betaNote": "This feature is currently in beta.",
"moreInfo": "More information can be found here",
"logs": "Logs",
"groupingField": "Grouping Field",
"groupingField": "Stack by field",
"insertAfter": "Insert After",
"insertBefore": "Insert Before",
"insertAbove": "Insert above",
@ -1082,6 +1082,7 @@
"projName": "Enter Base Name",
"selectGroupField": "Select a Grouping Field",
"selectGroupFieldNotFound": "No Single Select Field can be found. Please create one first.",
"selectCoverImageField": "Select a cover image field",
"selectGeoField": "Select a GeoData Field",
"notSelected": "-not selected-",
"selectGeoFieldNotFound": "No GeoData Field can be found. Please create one first.",

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

@ -115,6 +115,7 @@ export default defineConfig({
default: '0px 0px 4px 0px rgba(0, 0, 0, 0.08)',
hover: '0px 0px 4px 0px rgba(0, 0, 0, 0.24)',
selected: '0px 0px 0px 2px var(--ant-primary-color-outline)',
error: '0px 0px 0px 2px var(--ant-error-color-outline)',
},
colors: {
...windiColors,

5
packages/nocodb/src/models/GalleryView.ts

@ -95,8 +95,9 @@ export default class GalleryView implements GalleryType {
]);
insertObj.fk_cover_image_col_id =
view?.fk_cover_image_col_id ||
columns?.find((c) => c.uidt === UITypes.Attachment)?.id;
view?.fk_cover_image_col_id !== undefined
? view?.fk_cover_image_col_id
: columns?.find((c) => c.uidt === UITypes.Attachment)?.id;
insertObj.meta = {
fk_cover_image_object_fit:

5
packages/nocodb/src/models/KanbanView.ts

@ -104,8 +104,9 @@ export default class KanbanView implements KanbanType {
]);
insertObj.fk_cover_image_col_id =
view?.fk_cover_image_col_id ||
columns?.find((c) => c.uidt === UITypes.Attachment)?.id;
view?.fk_cover_image_col_id !== undefined
? view?.fk_cover_image_col_id
: columns?.find((c) => c.uidt === UITypes.Attachment)?.id;
insertObj.meta = {
fk_cover_image_object_fit:

Loading…
Cancel
Save