feat: Bitable P0 UX Polish + Agent Parity #23
|
|
@ -127,14 +127,13 @@ watch(
|
|||
// these have opaque values that don't form meaningful groups. Number/text/
|
||||
// date/select/multiselect all group by their scalar value.
|
||||
const selectableFields = computed(() =>
|
||||
props.fields.filter((f) => {
|
||||
return (
|
||||
props.fields.filter(
|
||||
(f) =>
|
||||
f.field_type !== 'formula' &&
|
||||
f.field_type !== 'lookup' &&
|
||||
f.field_type !== 'attachment' &&
|
||||
f.field_type !== 'image'
|
||||
)
|
||||
}),
|
||||
f.field_type !== 'image',
|
||||
),
|
||||
)
|
||||
|
||||
function fieldName(fieldId: string): string {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@
|
|||
</span>
|
||||
</template>
|
||||
<!-- Read-only renders -->
|
||||
<template v-if="!isEditable(f)">
|
||||
<template v-if="!isFieldEditable(f)">
|
||||
<SelectDisplay
|
||||
v-if="f.field_type === 'select' || f.field_type === 'multiselect'"
|
||||
:value="(record.values[f.id] as string | string[] | null | undefined)"
|
||||
|
|
@ -232,13 +232,9 @@ const showFooter = computed(
|
|||
() =>
|
||||
!!record.value &&
|
||||
fields.value.length > 0 &&
|
||||
fields.value.some(isEditable),
|
||||
fields.value.some(isFieldEditable),
|
||||
)
|
||||
|
||||
function isEditable(f: IBitableField): boolean {
|
||||
return isFieldEditable(f)
|
||||
}
|
||||
|
||||
function isAgentOwned(f: IBitableField): boolean {
|
||||
return f.owner === 'agent'
|
||||
}
|
||||
|
|
@ -264,7 +260,7 @@ watch(
|
|||
}
|
||||
if (!rec) return
|
||||
for (const f of fields.value) {
|
||||
if (!isEditable(f)) continue
|
||||
if (!isFieldEditable(f)) continue
|
||||
// Clone primitive values; clone arrays so multiselect edits don't
|
||||
// mutate the store's record.
|
||||
const v = rec.values[f.id]
|
||||
|
|
@ -284,7 +280,7 @@ watch(
|
|||
// Re-trigger hydration by reassigning via the record watch.
|
||||
for (const k of Object.keys(editBuffer)) delete editBuffer[k]
|
||||
for (const f of fields.value) {
|
||||
if (!isEditable(f)) continue
|
||||
if (!isFieldEditable(f)) continue
|
||||
const v = record.value.values[f.id]
|
||||
editBuffer[f.id] = Array.isArray(v) ? [...v] : (v ?? undefined)
|
||||
}
|
||||
|
|
@ -308,7 +304,7 @@ async function onSubmit(): Promise<void> {
|
|||
// from currentRecord.values inside updateRecordFields (preserves them).
|
||||
const edited: Record<string, unknown> = {}
|
||||
for (const f of fields.value) {
|
||||
if (!isEditable(f)) continue
|
||||
if (!isFieldEditable(f)) continue
|
||||
if (f.id in editBuffer) edited[f.id] = editBuffer[f.id]
|
||||
}
|
||||
const ok = await store.updateRecordFields(props.recordId, edited)
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@
|
|||
:fields="fields"
|
||||
/>
|
||||
<div class="view-config-panel__actions">
|
||||
<a-button type="primary" @click="saveGrouping">保存分组</a-button>
|
||||
<a-button type="primary" @click="saveU5Config">保存分组</a-button>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
:fields="fields"
|
||||
/>
|
||||
<div class="view-config-panel__actions">
|
||||
<a-button type="primary" @click="saveConditionalFormat">保存条件格式</a-button>
|
||||
<a-button type="primary" @click="saveU5Config">保存条件格式</a-button>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
|
@ -210,15 +210,9 @@ async function saveHidden(): Promise<void> {
|
|||
// U5: save group_by + conditional_formatting through the new
|
||||
// updateViewConfig action (which calls the same PATCH /views endpoint;
|
||||
// the route layer validates the U5 sub-keys and 422s on invalid input).
|
||||
async function saveGrouping(): Promise<void> {
|
||||
if (!props.view) return
|
||||
await store.updateViewConfig(props.view.id, {
|
||||
group_by: groupByItems.value,
|
||||
conditional_formatting: cfRules.value,
|
||||
})
|
||||
}
|
||||
|
||||
async function saveConditionalFormat(): Promise<void> {
|
||||
// Both tabs' save buttons call this — saving either tab persists both U5
|
||||
// keys together (matches the existing merge-then-PATCH behavior).
|
||||
async function saveU5Config(): Promise<void> {
|
||||
if (!props.view) return
|
||||
await store.updateViewConfig(props.view.id, {
|
||||
group_by: groupByItems.value,
|
||||
|
|
|
|||
|
|
@ -57,26 +57,18 @@ export function useResponsiveBreakpoint(): ResponsiveBreakpoint {
|
|||
isDesktop.value = !isMobile.value && !isTablet.value
|
||||
}
|
||||
|
||||
function createHandler(): () => void {
|
||||
return () => sync()
|
||||
}
|
||||
|
||||
const mobileHandler = createHandler()
|
||||
const tabletHandler = createHandler()
|
||||
const wideHandler = createHandler()
|
||||
|
||||
// sync 是稳定的函数引用,3 个 mql 共用同一 handler(每个 mql 独立 add/remove)。
|
||||
onMounted(() => {
|
||||
sync()
|
||||
// addEventListener('change', ...) 是现代标准 API
|
||||
mobileMql?.addEventListener('change', mobileHandler)
|
||||
tabletMql?.addEventListener('change', tabletHandler)
|
||||
wideMql?.addEventListener('change', wideHandler)
|
||||
mobileMql?.addEventListener('change', sync)
|
||||
tabletMql?.addEventListener('change', sync)
|
||||
wideMql?.addEventListener('change', sync)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
mobileMql?.removeEventListener('change', mobileHandler)
|
||||
tabletMql?.removeEventListener('change', tabletHandler)
|
||||
wideMql?.removeEventListener('change', wideHandler)
|
||||
mobileMql?.removeEventListener('change', sync)
|
||||
tabletMql?.removeEventListener('change', sync)
|
||||
wideMql?.removeEventListener('change', sync)
|
||||
})
|
||||
|
||||
return { isMobile, isTablet, isDesktop, isWide }
|
||||
|
|
|
|||
|
|
@ -259,11 +259,7 @@ export function findFirstMatchingRule(
|
|||
value: unknown,
|
||||
rules: ConditionalFormatRule[] | undefined,
|
||||
): ConditionalFormatRule | null {
|
||||
if (!rules || rules.length === 0) return null
|
||||
for (const rule of rules) {
|
||||
if (matchConditionalFormatRule(value, rule)) return rule
|
||||
}
|
||||
return null
|
||||
return rules?.find((rule) => matchConditionalFormatRule(value, rule)) ?? null
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -356,7 +352,8 @@ function sortGroupKeys(keys: string[], direction: GroupDirection): void {
|
|||
for (const k of keys) {
|
||||
if (k === '') continue
|
||||
const n = Number(k)
|
||||
if (k !== '' && !Number.isNaN(n) && Number.isFinite(n)) {
|
||||
// Number.isFinite excludes NaN/Infinity; '' already skipped above.
|
||||
if (Number.isFinite(n)) {
|
||||
numericKeys.push({ value: n, key: k })
|
||||
} else {
|
||||
stringKeys.push(k)
|
||||
|
|
|
|||
Loading…
Reference in New Issue