mirror of
https://github.com/nocodb/nocodb.git
synced 2026-06-02 01:51:44 +00:00
Merge pull request #13924 from nocodb/nc-feat/alphasort-toggle
feat(nc-gui): persistent alphabetize toggle for select field options
This commit is contained in:
@@ -63,6 +63,14 @@ const {
|
||||
defaultFormState,
|
||||
} = useColumnCreateStoreOrThrow()
|
||||
|
||||
// Patch colOptions into formState during setup so the child SelectOptions
|
||||
// (whose onMounted runs before this parent's onMounted) initializes its
|
||||
// local options from preload's pending edits rather than the stale column data.
|
||||
// Full preload merge (others + meta) still happens in onMounted below.
|
||||
if (props.preload?.colOptions) {
|
||||
formState.value.colOptions = { ...props.preload.colOptions }
|
||||
}
|
||||
|
||||
const { isAiFeaturesEnabled, isAiBetaFeaturesEnabled, aiIntegrationAvailable, aiLoading, aiError } = useNocoAi()
|
||||
|
||||
const {
|
||||
@@ -405,6 +413,8 @@ const saving = ref(false)
|
||||
|
||||
const warningVisible = ref(false)
|
||||
|
||||
const selectOptionsRef = ref<{ flushSort: () => void } | null>(null)
|
||||
|
||||
const saveSubmitted = async () => {
|
||||
if (readOnly.value) return
|
||||
let saved, savedColumn
|
||||
@@ -447,6 +457,9 @@ const saveSubmitted = async () => {
|
||||
async function onSubmit() {
|
||||
if (readOnly.value) return
|
||||
|
||||
selectOptionsRef.value?.flushSort()
|
||||
await nextTick()
|
||||
|
||||
// Show warning message if user tries to change type of column
|
||||
if (isEdit.value && formState.value.uidt !== column.value?.uidt) {
|
||||
warningVisible.value = true
|
||||
@@ -465,6 +478,7 @@ async function onSubmit() {
|
||||
|
||||
// focus and select the column name field
|
||||
const antInput = ref()
|
||||
|
||||
watchEffect(() => {
|
||||
if (antInput.value && formState.value && !readOnly.value) {
|
||||
// todo: replace setTimeout
|
||||
@@ -1411,6 +1425,7 @@ const unique = computed({
|
||||
<SmartsheetColumnUserOptions v-if="formState.uidt === UITypes.User" v-model:value="formState" :is-edit="isEdit" />
|
||||
<SmartsheetColumnSelectOptions
|
||||
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
|
||||
ref="selectOptionsRef"
|
||||
v-model:value="formState"
|
||||
:from-table-explorer="props.fromTableExplorer || false"
|
||||
/>
|
||||
|
||||
@@ -674,10 +674,7 @@ const handleScrollIntoView = () => {
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<GeneralSourceRestrictionTooltip
|
||||
message="Field cannot be upgraded."
|
||||
:enabled="!!isMetaReadOnly"
|
||||
>
|
||||
<GeneralSourceRestrictionTooltip message="Field cannot be upgraded." :enabled="!!isMetaReadOnly">
|
||||
<NcButton size="xs" type="primary" :disabled="isMetaReadOnly" @click="emit('upgrade')">
|
||||
{{ $t('general.upgrade') }}
|
||||
</NcButton>
|
||||
|
||||
@@ -69,6 +69,17 @@ const isColorCodeEnabled = computed({
|
||||
},
|
||||
})
|
||||
|
||||
const isAlphabetized = computed({
|
||||
get: () => {
|
||||
const metaObj = parseProp(vModel.value.meta)
|
||||
return metaObj.isAlphabetized === true
|
||||
},
|
||||
set: (val: boolean) => {
|
||||
const metaObj = parseProp(vModel.value.meta)
|
||||
vModel.value.meta = { ...metaObj, isAlphabetized: val }
|
||||
},
|
||||
})
|
||||
|
||||
const isKanban = inject(IsKanbanInj, ref(false))
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -180,6 +191,12 @@ const syncOptions = (saveChanges = false, submit = false, payload?: Option) => {
|
||||
vModel.value.colOptions.options = options.value
|
||||
.filter((op) => op.status !== 'remove')
|
||||
.sort((a, b) => {
|
||||
// On submit (e.g. saving a kanban stack) respect the Alphabetize toggle so the
|
||||
// persisted option order is alphabetical rather than the current rendered order.
|
||||
if (submit && isAlphabetized.value) {
|
||||
return (a.title ?? '').localeCompare(b.title ?? '')
|
||||
}
|
||||
|
||||
const renderA = renderedOptions.value.findIndex((el) => a.index !== undefined && el.index === a.index)
|
||||
const renderB = renderedOptions.value.findIndex((el) => a.index !== undefined && el.index === b.index)
|
||||
if (renderA === -1 || renderB === -1) return 0
|
||||
@@ -383,7 +400,7 @@ const predictOptions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const alphabetizeOptions = () => {
|
||||
const sortOptionsInPlace = () => {
|
||||
const activeOptions = options.value.filter((op) => op.status !== 'remove')
|
||||
|
||||
const alreadySorted = activeOptions.every(
|
||||
@@ -409,7 +426,11 @@ const alphabetizeOptions = () => {
|
||||
syncOptions()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Seed the local `options` list from the bound model. Runs on mount and again whenever
|
||||
// the underlying field changes while this component stays mounted (AI auto-suggest field
|
||||
// switch, where `colOptions` is swapped under us). Without re-seeding, the previously
|
||||
// selected field's options would stay on screen even though every other prop updated.
|
||||
function seedOptionsFromModel() {
|
||||
if (!vModel.value.colOptions?.options) {
|
||||
vModel.value.colOptions = {
|
||||
options: [],
|
||||
@@ -444,9 +465,26 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
const fndDefaultOption = options.value.filter((el) => el.title === vModel.value.cdf)
|
||||
if (fndDefaultOption.length) {
|
||||
defaultOption.value = vModel.value.uidt === UITypes.SingleSelect ? [fndDefaultOption[0]] : fndDefaultOption
|
||||
}
|
||||
defaultOption.value = fndDefaultOption.length
|
||||
? vModel.value.uidt === UITypes.SingleSelect
|
||||
? [fndDefaultOption[0]]
|
||||
: fndDefaultOption
|
||||
: []
|
||||
}
|
||||
|
||||
// Re-seed when the bound field switches in place (identity changes), e.g. toggling
|
||||
// between AI auto-suggested select fields. Local edits never change the identity key,
|
||||
// so this never fires mid-edit.
|
||||
watch(
|
||||
() => vModel.value.ai_temp_id ?? vModel.value.id ?? vModel.value.temp_id,
|
||||
() => {
|
||||
seedOptionsFromModel()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
seedOptionsFromModel()
|
||||
|
||||
if (isKanbanStack.value && isNewStack.value) {
|
||||
addNewOption()
|
||||
} else if (isKanbanStack.value) {
|
||||
@@ -498,6 +536,12 @@ if (!isKanbanStack.value) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
flushSort: () => {
|
||||
if (isAlphabetized.value) sortOptionsInPlace()
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -509,18 +553,16 @@ if (!isKanbanStack.value) {
|
||||
</NcSwitch>
|
||||
</div>
|
||||
|
||||
<NcButton
|
||||
v-e="['c:field:select:alphabetize']"
|
||||
type="text"
|
||||
size="small"
|
||||
:disabled="isSyncedField"
|
||||
@click.stop="alphabetizeOptions"
|
||||
>
|
||||
<template #icon>
|
||||
<GeneralIcon icon="ncArrowUpDown" class="h-4 w-4 opacity-80" />
|
||||
</template>
|
||||
{{ $t('labels.alphabetize') }}
|
||||
</NcButton>
|
||||
<div class="flex items-center">
|
||||
<NcSwitch
|
||||
v-model:checked="isAlphabetized"
|
||||
size="xsmall"
|
||||
:disabled="isSyncedField"
|
||||
@change="(v) => $e('c:field:select:alphabetize:toggle', { enabled: v })"
|
||||
>
|
||||
{{ $t('labels.alphabetize') }}
|
||||
</NcSwitch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -607,7 +649,7 @@ if (!isKanbanStack.value) {
|
||||
:list="renderedOptions"
|
||||
item-key="id"
|
||||
handle=".nc-child-draggable-icon"
|
||||
:disabled="isSyncedField"
|
||||
:disabled="isAlphabetized || isSyncedField"
|
||||
@change="onDragReorder"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
@@ -619,7 +661,8 @@ if (!isKanbanStack.value) {
|
||||
>
|
||||
<div
|
||||
v-if="!isKanban"
|
||||
class="nc-child-draggable-icon p-2 flex cursor-pointer text-nc-content-gray-subtle"
|
||||
class="nc-child-draggable-icon p-2 flex text-nc-content-gray-subtle"
|
||||
:class="isAlphabetized ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'"
|
||||
:data-testid="`select-option-column-handle-icon-${element.title}`"
|
||||
>
|
||||
<component :is="iconMap.dragVertical" small class="handle" />
|
||||
|
||||
@@ -933,6 +933,27 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// When the Alphabetize toggle is enabled, select option order is committed only at
|
||||
// submit time (mirrors EditOrAdd.onSubmit → SelectOptions.flushSort used by the single
|
||||
// column modal). The multi-field editor saves via ops instead of that submit path, so
|
||||
// sort here. Backend assigns option order by array index (Column.ts), so reordering the
|
||||
// array is sufficient.
|
||||
const sortAlphabetizedSelectOptions = () => {
|
||||
for (const op of ops.value) {
|
||||
if (op.op !== 'add' && op.op !== 'update') continue
|
||||
|
||||
const col = op.column
|
||||
if (![UITypes.SingleSelect, UITypes.MultiSelect].includes(col.uidt as UITypes)) continue
|
||||
|
||||
if (parseProp(col.meta).isAlphabetized !== true) continue
|
||||
|
||||
const colOptions = col.colOptions as SelectOptionsType | undefined
|
||||
if (!colOptions?.options?.length) continue
|
||||
|
||||
colOptions.options = [...colOptions.options].sort((a, b) => (a.title ?? '').localeCompare(b.title ?? ''))
|
||||
}
|
||||
}
|
||||
|
||||
const saveChanges = async () => {
|
||||
if (!isColumnsValid.value) {
|
||||
message.error(t('msg.error.multiFieldSaveValidation'))
|
||||
@@ -940,6 +961,8 @@ const saveChanges = async () => {
|
||||
} else if (!loading.value && !hasUnsavedChanges.value) {
|
||||
return
|
||||
}
|
||||
|
||||
sortAlphabetizedSelectOptions()
|
||||
try {
|
||||
if (!meta.value?.id) return
|
||||
|
||||
|
||||
@@ -21,8 +21,7 @@ import {
|
||||
*/
|
||||
const syncSystemColumnTitles = new Set<string>(SYNC_SYSTEM_COLUMN_TITLES)
|
||||
|
||||
const isSyncSystemColumnTitle = (title?: string | null): boolean =>
|
||||
!!title && syncSystemColumnTitles.has(title)
|
||||
const isSyncSystemColumnTitle = (title?: string | null): boolean => !!title && syncSystemColumnTitles.has(title)
|
||||
|
||||
const getSyncFrequency = (trigger: SyncTrigger, cron?: string) => {
|
||||
if (trigger === SyncTrigger.Manual) return 'Manual'
|
||||
|
||||
Reference in New Issue
Block a user