<script setup>
import { computed, defineComponent, inject, onBeforeUnmount, provide, ref, watch } from "vue";
import { provideCELContext, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
import { BREADCRUMB_EXTENSION_KEY } from "../config/breadcrumb-extension";
import { TABLE_COLUMN_LAYOUT_KEY } from "./column-layout";
import { Icon } from "@iconify/vue";
import { ParseResult, Schema } from "effect";
import { TableConfig, Pagination, ColumnGroup, tableDataSource, configureTableActionsScope, registerDataSourceRequestVars, registerDataSourceResponseVars, metadata as tableMetadata } from "./schema";
import { COLUMNS, findColumn as findColumnEntry } from "./utils/resolve";
import {
  useTreeDnd
} from "../../composables/useTreeDnd";
import { getStructFieldDescription, getStructFieldTitle } from "./utils/schema-meta";
import { provideEventAncestor } from "../../share/event-bus";
import { Markdown } from "../ui/markdown";
import { Badge } from "../ui/badge";
import { ScrollArea } from "../ui/scroll-area";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import { Separator } from "../ui/separator";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { ExpressionEditor } from "../ui/expression-editor";
import { Field, FieldLabel } from "../ui/field";
import { Locale } from "../ui/locale";
import { Switch } from "../ui/switch";
import { CommandItem, CommandGroup } from "../ui/command";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupCombobox,
  InputGroupInput,
  InputGroupNumberField
} from "../ui/input-group";
import ActionsConfigEditor from "../actions/config.vue";
import ShwfedFormConfig from "../form/config.vue";
import { defaultFormConfig } from "../form/schema";
import { Environment } from "../../vendor/cel-js/lib/index";
defineOptions({ name: "ShwfedTableConfig" });
function probeCELContext(register) {
  const probe = new Environment({ unlistedVariablesAreDyn: false });
  const baseline = new Set(probe.getDefinitions().variables.map((v) => v.name));
  register(probe);
  const out = {};
  for (const v of probe.getDefinitions().variables) {
    if (baseline.has(v.name)) continue;
    out[v.name] = {
      type: v.type,
      label: v.label ?? v.name,
      description: v.description ?? void 0,
      value: void 0
    };
  }
  return out;
}
const RowKeyCELContext = defineComponent({
  name: "RowKeyCELContext",
  setup(_, { slots }) {
    provideCELContext({
      row: {
        type: "dyn",
        label: "\u5F53\u524D\u884C",
        description: "\u5F53\u524D\u6E32\u67D3\u7684\u6574\u884C\u6570\u636E\uFF0C\u7C7B\u578B\u5728\u914D\u7F6E\u65F6\u672A\u77E5\uFF08`dyn`\uFF09\uFF0C\u5177\u4F53\u5B57\u6BB5\u53D6\u51B3\u4E8E\u63A5\u5165\u6570\u636E\u6E90\u3002",
        value: void 0
      },
      index: {
        type: "number",
        label: "\u884C\u5E8F\u53F7",
        description: "\u5F53\u524D\u884C\u5728\u6240\u6709\u6570\u636E\u4E2D\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB\u3002",
        value: 0
      }
    });
    return () => slots.default?.();
  }
});
const CellStyleCELContext = defineComponent({
  name: "CellStyleCELContext",
  setup(_, { slots }) {
    provideCELContext({
      row: {
        type: "dyn",
        label: "\u5F53\u524D\u884C",
        description: "\u5F53\u524D\u6E32\u67D3\u7684\u6574\u884C\u6570\u636E\uFF0C\u7C7B\u578B\u5728\u914D\u7F6E\u65F6\u672A\u77E5\uFF08`dyn`\uFF09\uFF0C\u5177\u4F53\u5B57\u6BB5\u53D6\u51B3\u4E8E\u63A5\u5165\u6570\u636E\u6E90\u3002",
        value: void 0
      },
      index: {
        type: "number",
        label: "\u884C\u5E8F\u53F7",
        description: "\u5F53\u524D\u884C\u5728\u6240\u6709\u6570\u636E\u4E2D\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB\u3002",
        value: 0
      },
      id: {
        type: "string",
        label: "\u5217 ID",
        description: "\u5F53\u524D\u5355\u5143\u683C\u6240\u5728\u5217\u7684 `id`\u3002",
        value: ""
      },
      selected: {
        type: "bool",
        label: "\u662F\u5426\u9009\u4E2D",
        description: "\u5F53\u524D\u884C\u662F\u5426\u88AB\u52FE\u9009\u3002",
        value: false
      },
      pinned: {
        type: "string",
        label: "\u5217\u56FA\u5B9A\u65B9\u5411",
        description: "\u5217\u7684\u56FA\u5B9A\u65B9\u5411\uFF1A`left`\u3001`right`\uFF0C\u672A\u56FA\u5B9A\u65F6\u4E3A `false`\u3002",
        value: false
      }
    });
    return () => slots.default?.();
  }
});
const RequestCELContext = defineComponent({
  name: "RequestCELContext",
  setup(_, { slots }) {
    provideCELContext(probeCELContext(registerDataSourceRequestVars));
    return () => slots.default?.();
  }
});
const JsonCELContext = defineComponent({
  name: "JsonCELContext",
  setup(_, { slots }) {
    provideCELContext(probeCELContext(registerDataSourceResponseVars));
    return () => slots.default?.();
  }
});
const TableActionsCELContext = defineComponent({
  name: "TableActionsCELContext",
  setup(_, { slots }) {
    provideCELContext({
      selected: {
        type: "list<dyn>",
        label: "\u5DF2\u9009\u884C",
        description: "\u5F53\u524D\u5DF2\u52FE\u9009\u7684\u6240\u6709\u884C\uFF08`row` \u7684\u6570\u7EC4\uFF09\u3002",
        value: []
      }
    });
    return () => slots.default?.();
  }
});
const config = defineModel({ type: Object, ...{ required: true } });
const props = defineProps({
  configure: { type: Function, required: false },
  hideDisplayName: { type: Boolean, required: false },
  hideDataSource: { type: Boolean, required: false }
});
const configure = props.configure ?? (() => {
});
function configureTableActions(env) {
  configureTableActionsScope(env, configure);
}
function configureTableQuery(env) {
  configure(env);
}
const tableConfigSchema = TableConfig(configure);
const dataSourceSchema = tableDataSource(configure);
const pickerEntries = computed(() => COLUMNS.filter((e) => !e.deprecated));
const advertisedHostContext = probeCELContext(configure);
provideCELContext(advertisedHostContext);
const generalFieldTitle = (f) => getStructFieldTitle(tableConfigSchema, f) ?? f;
const generalFieldDescription = (f) => getStructFieldDescription(tableConfigSchema, f);
const paginationFieldTitle = (f) => getStructFieldTitle(Pagination, f) ?? f;
const paginationFieldDescription = (f) => getStructFieldDescription(Pagination, f);
const groupFieldTitle = (f) => getStructFieldTitle(ColumnGroup, f) ?? f;
const groupFieldDescription = (f) => getStructFieldDescription(ColumnGroup, f);
const dataSourceFieldTitle = (f) => getStructFieldTitle(dataSourceSchema, f) ?? f;
const dataSourceFieldDescription = (f) => getStructFieldDescription(dataSourceSchema, f);
const GROUP_DRAG_KIND = "group";
const COLUMN_DRAG_KIND = "column";
const editingColumns = computed({
  get: () => config.value.columns ?? [],
  set: (next) => {
    config.value = { ...config.value, columns: next };
  }
});
const editingGroups = computed({
  get: () => config.value.groups ?? [],
  set: (next) => {
    config.value = { ...config.value, groups: next };
  }
});
function snapshot() {
  return {
    columns: [...config.value.columns ?? []],
    groups: [...config.value.groups ?? []],
    initialState: config.value.initialState ? { ...config.value.initialState } : void 0
  };
}
function commit(w) {
  const next = {
    ...config.value,
    columns: w.columns,
    groups: w.groups
  };
  if (w.initialState && Object.keys(w.initialState).length > 0) {
    next.initialState = w.initialState;
  } else {
    delete next.initialState;
  }
  config.value = next;
}
function normalizeForEmit(general) {
  const out = { ...general };
  const ds = out.dataSource;
  let emittedDs;
  if (ds) {
    const nextDs = { ...ds };
    if (typeof nextDs.request === "string" && nextDs.request.trim() === "") delete nextDs.request;
    if (typeof nextDs.total === "string" && nextDs.total.trim() === "") delete nextDs.total;
    if (typeof nextDs.data === "string" && nextDs.data.trim().length > 0) {
      emittedDs = nextDs;
    }
  }
  if (emittedDs) out.dataSource = emittedDs;
  else delete out.dataSource;
  const query = out.query;
  if (query && (!Array.isArray(query.fields) || query.fields.length === 0)) {
    delete out.query;
  }
  return out;
}
function readGeneral() {
  const { columns: _c, groups: _g, kind: _k, ...general } = config.value ?? {};
  const cloned = JSON.parse(JSON.stringify(general));
  if (!cloned.dataSource) cloned.dataSource = { data: "" };
  return cloned;
}
function writeGeneralPatch(patch) {
  const next = normalizeForEmit({ ...readGeneral(), ...patch });
  config.value = {
    ...config.value,
    ...next
  };
}
const editingGeneralConfig = computed(() => readGeneral());
const selected = ref({ kind: "general" });
const fieldErrors = ref({});
watch(
  config,
  (value) => {
    fieldErrors.value = {};
    const result = Schema.decodeUnknownEither(tableConfigSchema)(value);
    if (result._tag === "Left") {
      const issues = ParseResult.ArrayFormatter.formatErrorSync(result.left);
      for (const issue of issues) {
        const pathKey = issue.path.map(String).join(".");
        if (!fieldErrors.value[pathKey]) {
          fieldErrors.value[pathKey] = issue.message;
        }
      }
    }
  },
  { immediate: true, deep: true }
);
const selectedGeneral = computed(() => selected.value.kind === "general");
const selectedActions = computed(() => selected.value.kind === "actions");
const selectedBottomActions = computed(() => selected.value.kind === "bottomActions");
const selectedQuery = computed(() => selected.value.kind === "query");
const columnFullPane = ref(false);
provide(TABLE_COLUMN_LAYOUT_KEY, { fullPane: columnFullPane });
const tableEventAncestor = computed(() => {
  const id = config.value.id;
  if (!id) return null;
  const displayName = config.value.displayName;
  return {
    instanceId: id,
    // Left undefined when the configurer wrote no 内部名称 — the trigger UI
    // then shows just the type name rather than a redundant 「表格」.
    name: displayName && displayName.length > 0 ? displayName : void 0,
    typeName: tableMetadata.name,
    typeIcon: tableMetadata.icon,
    operations: tableMetadata.operations
  };
});
provideEventAncestor(tableEventAncestor);
const tableScopeAncestor = computed(() => {
  const id = config.value.id;
  if (!id) return null;
  const displayName = config.value.displayName;
  return {
    id,
    name: displayName && displayName.length > 0 ? displayName : void 0,
    typeName: tableMetadata.name,
    typeIcon: tableMetadata.icon,
    members: [
      { key: "row", label: "\u5F53\u524D\u884C", type: "dyn", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u6574\u884C\u6570\u636E" },
      { key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" }
    ],
    // Each row-provider rebinds the relative `selections` to its own per-row
    // registry (see row-provider.vue), so a table row shadows an enclosing
    // form's selection options even though the table exposes no `selections`
    // member of its own — list it here so that form's options stay offerable to
    // a cell expression inside this table.
    shadows: ["row", "index", SELECTIONS_VAR]
  };
});
provideScopeAncestor(tableScopeAncestor);
const isDrilled = computed(
  () => selectedActions.value || selectedBottomActions.value || selectedQuery.value || columnFullPane.value
);
function findGroup(id) {
  return editingGroups.value.find((g) => g.id === id) ?? null;
}
function findColumn(id) {
  return editingColumns.value.find((c) => c.id === id) ?? null;
}
const selectedGroupRef = computed(() => {
  if (selected.value.kind !== "group") return null;
  return findGroup(selected.value.id);
});
const selectedColumnRef = computed(() => {
  if (selected.value.kind !== "column") return null;
  return findColumn(selected.value.id);
});
const selectedColumnEntry = computed(() => {
  const c = selectedColumnRef.value;
  if (!c) return void 0;
  return findColumnEntry(c.type, c.compatibilityDate);
});
const selectionRoster = computed(
  () => editingColumns.value.filter((c) => findColumnEntry(c.type, c.compatibilityDate)?.metadata?.selection).map((c) => ({ id: c.id, name: columnLabel(c) }))
);
provideSelectionRoster(selectionRoster);
const pinnedLeftIds = computed(() => {
  const ids = editingGeneralConfig.value.initialState?.columnPinning?.left;
  return new Set(Array.isArray(ids) ? ids : []);
});
const pinnedRightIds = computed(() => {
  const ids = editingGeneralConfig.value.initialState?.columnPinning?.right;
  return new Set(Array.isArray(ids) ? ids : []);
});
function columnBand(id) {
  if (pinnedLeftIds.value.has(id)) return "left";
  if (pinnedRightIds.value.has(id)) return "right";
  return "main";
}
function groupBand(groupId) {
  const members = editingColumns.value.filter((c) => c.groupId === groupId);
  if (members.length === 0) return "main";
  const first = columnBand(members[0].id);
  return members.every((m) => columnBand(m.id) === first) ? first : "main";
}
function sameArray(a, b) {
  if (!b) return a.length === 0;
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
  return true;
}
function pinnedLeftSetOf(w) {
  const arr = w.initialState?.columnPinning?.left;
  return new Set(Array.isArray(arr) ? arr : []);
}
function pinnedRightSetOf(w) {
  const arr = w.initialState?.columnPinning?.right;
  return new Set(Array.isArray(arr) ? arr : []);
}
function writePinArraysW(w, nextLeft, nextRight) {
  const prevLeft = w.initialState?.columnPinning?.left;
  const prevRight = w.initialState?.columnPinning?.right;
  if (sameArray(nextLeft, prevLeft) && sameArray(nextRight, prevRight)) return;
  const initialState = { ...w.initialState ?? {} };
  const pinning = { ...initialState.columnPinning ?? {} };
  if (nextLeft.length > 0) pinning.left = nextLeft;
  else delete pinning.left;
  if (nextRight.length > 0) pinning.right = nextRight;
  else delete pinning.right;
  if (Object.keys(pinning).length > 0) initialState.columnPinning = pinning;
  else delete initialState.columnPinning;
  w.initialState = Object.keys(initialState).length > 0 ? initialState : void 0;
}
function reconcilePinningW(w) {
  const cols = w.columns;
  const leftSet = pinnedLeftSetOf(w);
  const rightSet = pinnedRightSetOf(w);
  const nextLeft = [];
  let i = 0;
  while (i < cols.length && leftSet.has(cols[i].id)) {
    nextLeft.push(cols[i].id);
    i++;
  }
  const tail = [];
  let j = cols.length - 1;
  while (j >= i && rightSet.has(cols[j].id)) {
    tail.push(cols[j].id);
    j--;
  }
  writePinArraysW(w, nextLeft, tail.reverse());
}
function setColumnPinW(w, id, side) {
  const left = [...pinnedLeftSetOf(w)].filter((x) => x !== id);
  const right = [...pinnedRightSetOf(w)].filter((x) => x !== id);
  if (side === "left") left.push(id);
  if (side === "right") right.push(id);
  writePinArraysW(w, left, right);
}
function setGroupPinW(w, groupId, side) {
  const members = w.columns.filter((c) => c.groupId === groupId);
  const memberIds = new Set(members.map((m) => m.id));
  const left = [...pinnedLeftSetOf(w)].filter((x) => !memberIds.has(x));
  const right = [...pinnedRightSetOf(w)].filter((x) => !memberIds.has(x));
  if (side === "left") for (const m of members) left.push(m.id);
  if (side === "right") for (const m of members) right.push(m.id);
  writePinArraysW(w, left, right);
}
const treeEntries = computed(() => {
  const out = [];
  let lastGid;
  let lastBand;
  for (const col of editingColumns.value) {
    const gid = col.groupId && findGroup(col.groupId) ? col.groupId : void 0;
    const band = pinnedLeftIds.value.has(col.id) ? "left" : pinnedRightIds.value.has(col.id) ? "right" : "main";
    if (gid && (gid !== lastGid || band !== lastBand)) {
      out.push({ kind: "group-header", groupId: gid, band });
    }
    out.push({ kind: "column", columnId: col.id, groupId: gid, band });
    lastGid = gid;
    lastBand = band;
  }
  return out;
});
const renderEntries = computed(() => {
  const left = treeEntries.value.filter((e) => e.band === "left");
  const main = treeEntries.value.filter((e) => e.band === "main");
  const right = treeEntries.value.filter((e) => e.band === "right");
  const out = [];
  if (left.length > 0) {
    out.push({ kind: "band-caption", band: "left", text: "\u56FA\u5B9A\u5728\u5DE6\u4FA7" });
    out.push(...left);
  }
  if (left.length > 0 && (main.length > 0 || right.length > 0)) {
    out.push({ kind: "band-separator", id: "sep-left" });
  }
  out.push(...main);
  if (right.length > 0) {
    if (main.length > 0 || left.length > 0) {
      out.push({ kind: "band-separator", id: "sep-right" });
    }
    out.push({ kind: "band-caption", band: "right", text: "\u56FA\u5B9A\u5728\u53F3\u4FA7" });
    out.push(...right);
  }
  return out;
});
function entryKey(entry) {
  switch (entry.kind) {
    case "group-header":
      return `g-${entry.band}-${entry.groupId}`;
    case "column":
      return `c-${entry.columnId}`;
    case "band-caption":
      return `cap-${entry.band}`;
    case "band-separator":
      return entry.id;
  }
}
function groupLabel(group) {
  if (Array.isArray(group?.title)) {
    const zh = group.title.find((t) => t.locale === "zh");
    const msg = zh?.message || group.title[0]?.message;
    if (msg) return msg;
  }
  return "\u672A\u547D\u540D\u5206\u7EC4";
}
function columnLabel(col) {
  if (!col) return "(\u672A\u77E5)";
  if (Array.isArray(col.title)) {
    const zh = col.title.find((t) => t.locale === "zh");
    const msg = zh?.message || col.title[0]?.message;
    if (msg) return msg;
  }
  return getColumnTypeLabel(col.type, col.compatibilityDate);
}
function getColumnTypeLabel(type, compatibilityDate) {
  return findColumnEntry(type, compatibilityDate)?.metadata.name ?? type;
}
function getColumnDefaults(id, type, compatibilityDate) {
  const entry = findColumnEntry(type, compatibilityDate);
  return { ...entry?.defaults?.() ?? {}, id, type, compatibilityDate };
}
function addColumn(columnType, compatibilityDate) {
  const col = getColumnDefaults(crypto.randomUUID(), columnType, compatibilityDate);
  editingColumns.value = [...editingColumns.value, col];
  selected.value = { kind: "column", id: col.id };
}
function pruneEmptyGroupW(w, groupId) {
  const hasMembers = w.columns.some((c) => c.groupId === groupId);
  if (hasMembers) return;
  w.groups = w.groups.filter((g) => g.id !== groupId);
}
function deleteGroup(groupId) {
  const w = snapshot();
  w.columns = w.columns.map((c) => {
    if (c.groupId !== groupId) return c;
    const { groupId: _g, ...rest } = c;
    return rest;
  });
  w.groups = w.groups.filter((g) => g.id !== groupId);
  reconcilePinningW(w);
  commit(w);
  resolveSelection();
}
function deleteColumn(columnId) {
  const w = snapshot();
  const col = w.columns.find((c) => c.id === columnId);
  w.columns = w.columns.filter((c) => c.id !== columnId);
  if (col?.groupId) pruneEmptyGroupW(w, col.groupId);
  reconcilePinningW(w);
  commit(w);
  resolveSelection();
}
function resolveSelection() {
  const s = selected.value;
  if (s.kind === "general") return;
  if (s.kind === "actions") return;
  if (s.kind === "bottomActions") return;
  if (s.kind === "query") return;
  if (s.kind === "group" && findGroup(s.id)) return;
  if (s.kind === "column" && findColumn(s.id)) return;
  selected.value = { kind: "general" };
}
function selectGeneral() {
  selected.value = { kind: "general" };
}
function selectActions() {
  selected.value = { kind: "actions" };
}
function selectBottomActions() {
  selected.value = { kind: "bottomActions" };
}
function selectQuery() {
  selected.value = { kind: "query" };
}
function selectGroup(id) {
  selected.value = { kind: "group", id };
}
function selectColumn(id) {
  selected.value = { kind: "column", id };
}
const breadcrumbExt = inject(BREADCRUMB_EXTENSION_KEY, null);
if (breadcrumbExt) {
  const crumb = breadcrumbExt.add();
  watch(
    [selected, columnFullPane],
    ([s, full]) => {
      if (s.kind === "actions") {
        crumb.label.value = generalFieldTitle("actions");
        crumb.back.value = selectGeneral;
      } else if (s.kind === "bottomActions") {
        crumb.label.value = generalFieldTitle("bottomActions");
        crumb.back.value = selectGeneral;
      } else if (s.kind === "query") {
        crumb.label.value = generalFieldTitle("query");
        crumb.back.value = selectGeneral;
      } else if (s.kind === "column" && full) {
        const col = findColumn(s.id);
        crumb.label.value = col ? `\u7F16\u8F91${columnLabel(col)}` : "\u7F16\u8F91\u5217";
        crumb.back.value = selectGeneral;
      } else {
        crumb.label.value = null;
        crumb.back.value = null;
      }
    },
    { immediate: true }
  );
  onBeforeUnmount(() => {
    crumb.dispose();
  });
}
function patchColumn(id, next) {
  editingColumns.value = editingColumns.value.map(
    (c) => c.id === id ? { ...next, id } : c
  );
}
function patchSelectedColumn(next) {
  if (selected.value.kind !== "column") return;
  patchColumn(selected.value.id, next);
}
function patchSelectedGroup(next) {
  if (selected.value.kind !== "group") return;
  const id = selected.value.id;
  editingGroups.value = editingGroups.value.map(
    (g) => g.id === id ? { ...g, ...next } : g
  );
}
const PIN_LEFT_ZONE = "pin-left-zone";
const PIN_RIGHT_ZONE = "pin-right-zone";
function reorderColumnNextW(w, sourceId, targetId, edge) {
  if (sourceId === targetId) return;
  const src = w.columns.find((c) => c.id === sourceId);
  const tgt = w.columns.find((c) => c.id === targetId);
  if (!src || !tgt) return;
  const nextGroupId = tgt.groupId;
  const next = { ...src };
  if (nextGroupId) next.groupId = nextGroupId;
  else delete next.groupId;
  const formerGroupId = src.groupId && src.groupId !== nextGroupId ? src.groupId : void 0;
  const without = w.columns.filter((c) => c.id !== sourceId);
  const tgtIdx = without.findIndex((c) => c.id === targetId);
  if (tgtIdx < 0) {
    w.columns = [...without, next];
  } else {
    const insertAt = edge === "reorder-above" ? tgtIdx : tgtIdx + 1;
    const arr = [...without];
    arr.splice(insertAt, 0, next);
    w.columns = arr;
  }
  if (formerGroupId) pruneEmptyGroupW(w, formerGroupId);
}
function reorderColumnAroundGroupW(w, sourceId, targetGroupId, edge) {
  const src = w.columns.find((c) => c.id === sourceId);
  if (!src) return;
  const formerGroupId = src.groupId;
  const next = { ...src };
  delete next.groupId;
  const without = w.columns.filter((c) => c.id !== sourceId);
  const memberIndices = [];
  without.forEach((c, i) => {
    if (c.groupId === targetGroupId) memberIndices.push(i);
  });
  if (memberIndices.length === 0) return;
  const insertAt = edge === "reorder-above" ? memberIndices[0] : memberIndices[memberIndices.length - 1] + 1;
  const arr = [...without];
  arr.splice(insertAt, 0, next);
  w.columns = arr;
  if (formerGroupId && formerGroupId !== targetGroupId) pruneEmptyGroupW(w, formerGroupId);
}
function assignColumnToGroupW(w, sourceId, groupId) {
  const src = w.columns.find((c) => c.id === sourceId);
  if (!src || src.groupId === groupId) return;
  const formerGroupId = src.groupId;
  const next = { ...src, groupId };
  const without = w.columns.filter((c) => c.id !== sourceId);
  const firstMemberIdx = without.findIndex((c) => c.groupId === groupId);
  const arr = [...without];
  if (firstMemberIdx >= 0) arr.splice(firstMemberIdx, 0, next);
  else arr.push(next);
  w.columns = arr;
  if (formerGroupId) pruneEmptyGroupW(w, formerGroupId);
}
function moveGroupBlockW(w, sourceGroupId, target, edge) {
  const block = w.columns.filter((c) => c.groupId === sourceGroupId);
  if (block.length === 0) return;
  const rest = w.columns.filter((c) => c.groupId !== sourceGroupId);
  let anchorIdx;
  if (target.kind === "column") {
    if (block.some((b) => b.id === target.id)) return;
    anchorIdx = rest.findIndex((c) => c.id === target.id);
  } else {
    if (target.id === sourceGroupId) return;
    const firstIdx = rest.findIndex((c) => c.groupId === target.id);
    if (firstIdx < 0) return;
    const size = rest.filter((c) => c.groupId === target.id).length;
    anchorIdx = edge === "reorder-above" ? firstIdx : firstIdx + size - 1;
  }
  if (anchorIdx < 0) return;
  const insertAt = edge === "reorder-above" ? anchorIdx : anchorIdx + 1;
  const arr = [...rest];
  arr.splice(insertAt, 0, ...block);
  w.columns = arr;
}
function wrapColumnsInNewGroupW(w, sourceId, targetId) {
  if (sourceId === targetId) return void 0;
  const src = w.columns.find((c) => c.id === sourceId);
  const tgt = w.columns.find((c) => c.id === targetId);
  if (!src || !tgt) return void 0;
  if (src.groupId || tgt.groupId) return void 0;
  const newGroup = { id: crypto.randomUUID() };
  w.groups = [...w.groups, newGroup];
  const srcUpdated = { ...src, groupId: newGroup.id };
  const tgtUpdated = { ...tgt, groupId: newGroup.id };
  const without = w.columns.filter((c) => c.id !== sourceId);
  const tgtIdx = without.findIndex((c) => c.id === targetId);
  if (tgtIdx < 0) return void 0;
  const arr = [...without];
  arr[tgtIdx] = tgtUpdated;
  arr.splice(tgtIdx, 0, srcUpdated);
  w.columns = arr;
  return newGroup.id;
}
function pinColumnToW(w, id, side) {
  const col = w.columns.find((c) => c.id === id);
  if (!col) return;
  const formerGroupId = col.groupId;
  const detached = { ...col };
  if (detached.groupId) delete detached.groupId;
  setColumnPinW(w, id, side);
  const without = w.columns.filter((c) => c.id !== id);
  const left = pinnedLeftSetOf(w);
  const right = pinnedRightSetOf(w);
  let insertAt;
  if (side === "left") {
    let last = -1;
    for (let k = 0; k < without.length; k++) {
      if (left.has(without[k].id)) last = k;
    }
    insertAt = last + 1;
  } else {
    let first = without.length;
    for (let k = 0; k < without.length; k++) {
      if (right.has(without[k].id)) {
        first = k;
        break;
      }
    }
    insertAt = first;
  }
  const arr = [...without];
  arr.splice(insertAt, 0, detached);
  w.columns = arr;
  if (formerGroupId) pruneEmptyGroupW(w, formerGroupId);
}
function pinGroupToW(w, groupId, side) {
  const members = w.columns.filter((c) => c.groupId === groupId);
  if (members.length === 0) return;
  const memberIds = new Set(members.map((m) => m.id));
  setGroupPinW(w, groupId, side);
  const without = w.columns.filter((c) => !memberIds.has(c.id));
  const left = pinnedLeftSetOf(w);
  const right = pinnedRightSetOf(w);
  let insertAt;
  if (side === "left") {
    let last = -1;
    for (let k = 0; k < without.length; k++) {
      if (left.has(without[k].id)) last = k;
    }
    insertAt = last + 1;
  } else {
    let first = without.length;
    for (let k = 0; k < without.length; k++) {
      if (right.has(without[k].id)) {
        first = k;
        break;
      }
    }
    insertAt = first;
  }
  const arr = [...without];
  arr.splice(insertAt, 0, ...members);
  w.columns = arr;
}
function targetBand(target) {
  return target.kind === GROUP_DRAG_KIND ? groupBand(target.id) : columnBand(target.id);
}
function bandToPin(band) {
  return band === "main" ? void 0 : band;
}
function onRowDrop({ source, target, instruction }) {
  if (source.kind === GROUP_DRAG_KIND) {
    const w = snapshot();
    if (instruction === "reorder-above" || instruction === "reorder-below") {
      if (target.kind === GROUP_DRAG_KIND) {
        moveGroupBlockW(w, source.id, { kind: "group", id: target.id }, instruction);
      } else if (target.kind === COLUMN_DRAG_KIND) {
        moveGroupBlockW(w, source.id, { kind: "column", id: target.id }, instruction);
      }
      setGroupPinW(w, source.id, bandToPin(targetBand(target)));
    }
    reconcilePinningW(w);
    commit(w);
    return;
  }
  if (source.kind === COLUMN_DRAG_KIND) {
    if (target.kind === GROUP_DRAG_KIND) {
      const w = snapshot();
      if (instruction === "make-child") {
        assignColumnToGroupW(w, source.id, target.id);
      } else if (instruction === "reorder-above" || instruction === "reorder-below") {
        reorderColumnAroundGroupW(w, source.id, target.id, instruction);
      } else return;
      setColumnPinW(w, source.id, bandToPin(groupBand(target.id)));
      reconcilePinningW(w);
      commit(w);
      return;
    }
    if (target.kind === COLUMN_DRAG_KIND) {
      const w = snapshot();
      let newGroupId;
      if (instruction === "make-child") {
        newGroupId = wrapColumnsInNewGroupW(w, source.id, target.id);
        setColumnPinW(w, source.id, bandToPin(columnBand(target.id)));
      } else if (instruction === "reorder-above" || instruction === "reorder-below") {
        reorderColumnNextW(w, source.id, target.id, instruction);
        setColumnPinW(w, source.id, bandToPin(columnBand(target.id)));
      }
      reconcilePinningW(w);
      commit(w);
      if (newGroupId) selected.value = { kind: "group", id: newGroupId };
    }
  }
}
function onZoneDrop({ source, zoneId }) {
  if (zoneId !== PIN_LEFT_ZONE && zoneId !== PIN_RIGHT_ZONE) return;
  const side = zoneId === PIN_LEFT_ZONE ? "left" : "right";
  const w = snapshot();
  if (source.kind === COLUMN_DRAG_KIND) {
    pinColumnToW(w, source.id, side);
  } else if (source.kind === GROUP_DRAG_KIND) {
    pinGroupToW(w, source.id, side);
  }
  commit(w);
}
const dnd = useTreeDnd({ onRowDrop, onZoneDrop });
const pickDragHandle = (el) => el.querySelector(".drag-handle");
function pinZoneConfig(side) {
  return {
    data: { side },
    canDrop: (src) => {
      if (src.kind === COLUMN_DRAG_KIND) {
        return columnBand(src.id) !== side;
      }
      if (src.kind === GROUP_DRAG_KIND) {
        return groupBand(src.id) !== side;
      }
      return false;
    }
  };
}
function groupRowConfig(groupId) {
  const band = groupBand(groupId);
  return {
    kind: GROUP_DRAG_KIND,
    data: { band },
    canDrop: (src) => {
      if (src.kind === GROUP_DRAG_KIND) return src.id !== groupId;
      if (src.kind === COLUMN_DRAG_KIND) return true;
      return false;
    },
    blockInstructions: (src) => {
      const block = ["reparent"];
      if (src.kind === GROUP_DRAG_KIND) {
        block.push("make-child");
        if (src.id === groupId) block.push("reorder-above", "reorder-below");
      }
      return block;
    },
    resolveInstruction: ({ input, element, source }) => {
      const rect = element.getBoundingClientRect();
      const relY = input.clientY - rect.top;
      if (source.kind === COLUMN_DRAG_KIND) {
        const third = rect.height / 3;
        if (relY < third) return "reorder-above";
        if (relY > third * 2) return "reorder-below";
        return "make-child";
      }
      return relY < rect.height / 2 ? "reorder-above" : "reorder-below";
    },
    dragHandle: pickDragHandle
  };
}
function columnRowConfig(columnId) {
  const col = findColumn(columnId);
  const colGroupId = col?.groupId;
  const band = columnBand(columnId);
  return {
    kind: COLUMN_DRAG_KIND,
    data: { groupId: colGroupId, band },
    canDrop: (src) => {
      if (src.kind === COLUMN_DRAG_KIND) return src.id !== columnId;
      if (src.kind === GROUP_DRAG_KIND) {
        return colGroupId !== src.id;
      }
      return false;
    },
    blockInstructions: (src) => {
      const block = ["reparent"];
      if (src.id === columnId) {
        block.push("reorder-above", "reorder-below", "make-child");
        return block;
      }
      const targetUngrouped = !colGroupId;
      const sourceUngrouped = src.kind === COLUMN_DRAG_KIND && !src.data.groupId;
      if (!(targetUngrouped && sourceUngrouped)) block.push("make-child");
      return block;
    },
    resolveInstruction: ({ input, element, source }) => {
      const rect = element.getBoundingClientRect();
      const relY = input.clientY - rect.top;
      const third = rect.height / 3;
      const allowMakeChild = source.kind === COLUMN_DRAG_KIND && source.id !== columnId && !colGroupId && !source.data.groupId;
      if (allowMakeChild) {
        if (relY < third) return "reorder-above";
        if (relY > third * 2) return "reorder-below";
        return "make-child";
      }
      return relY < rect.height / 2 ? "reorder-above" : "reorder-below";
    },
    dragHandle: pickDragHandle
  };
}
function parsePageSizes(value) {
  return value.split(",").map((s) => Number.parseInt(s.trim(), 10)).filter((n) => !Number.isNaN(n) && n > 0);
}
function updateGeneralOptionalString(key, value) {
  const cur = readGeneral();
  if (value.trim() === "") {
    const { [key]: _, ...rest } = cur;
    config.value = {
      ...config.value,
      ...normalizeForEmit(rest)
    };
    if (key in config.value) {
      const { [key]: _drop, ...rest2 } = config.value;
      config.value = rest2;
    }
  } else {
    writeGeneralPatch({ [key]: value });
  }
}
function setGeneralDisplayName(value) {
  const next = String(value ?? "");
  writeGeneralPatch({ displayName: next.length > 0 ? next : void 0 });
}
function setDataSourceField(key, value) {
  const cur = readGeneral();
  const ds = { ...cur.dataSource ?? {} };
  ds[key] = value;
  writeGeneralPatch({ dataSource: ds });
}
function updateDataSourceOptional(key, value) {
  const cur = readGeneral();
  const { [key]: _omit, ...rest } = cur.dataSource ?? {};
  const ds = value.trim() === "" ? rest : { ...rest, [key]: value };
  writeGeneralPatch({ dataSource: ds });
}
function setPaginationEnabled(enabled) {
  if (enabled) {
    if (config.value.pagination !== void 0) return;
    writeGeneralPatch({ pagination: { pageSize: 100 } });
    return;
  }
  const cur = config.value;
  if (!cur || cur.pagination === void 0) return;
  const { pagination: _drop, ...rest } = cur;
  config.value = rest;
}
function setPersistQueryEnabled(enabled) {
  if (enabled) {
    writeGeneralPatch({ persistQuery: true });
    return;
  }
  const cur = config.value;
  if (!cur || cur.persistQuery === void 0) return;
  const { persistQuery: _drop, ...rest } = cur;
  config.value = rest;
}
function setPageSize(value) {
  const cur = readGeneral();
  if (!cur.pagination) return;
  const n = typeof value === "number" ? value : Number(value);
  if (!Number.isFinite(n) || n <= 0) return;
  writeGeneralPatch({ pagination: { ...cur.pagination, pageSize: n } });
}
function setPageSizes(value) {
  const parsed = parsePageSizes(value);
  const cur = readGeneral();
  if (!cur.pagination) return;
  const p = { ...cur.pagination };
  if (parsed.length === 0) delete p.pageSizes;
  else p.pageSizes = parsed;
  writeGeneralPatch({ pagination: p });
}
function setPaginationLocale(key, value) {
  const cur = readGeneral();
  if (!cur.pagination) return;
  const p = { ...cur.pagination };
  if (value == null) {
    if (key === "left") delete p.left;
    else delete p.right;
  } else p[key] = value;
  writeGeneralPatch({ pagination: p });
}
function getError(fieldPath) {
  return fieldErrors.value[fieldPath];
}
const makeTableActionsValue = (key) => computed({
  get: () => {
    const a = editingGeneralConfig.value[key] ?? {};
    return {
      kind: "shwfed.component.action",
      size: a.size ?? "default",
      gap: a.gap ?? 4,
      style: a.style,
      groups: a.groups ?? [],
      items: a.items ?? []
    };
  },
  set: (next) => {
    const { size, gap, style, groups, items } = next;
    const hasContent = groups && groups.length > 0 || items && items.length > 0 || typeof style === "string" && style.length > 0 || size !== void 0 && size !== "default";
    if (!hasContent) {
      const cur = readGeneral();
      const { [key]: _drop, ...rest } = cur;
      const merged = normalizeForEmit(rest);
      const { [key]: _stale, ...cleared } = { ...config.value, ...merged };
      config.value = cleared;
      return;
    }
    const actions = { size, gap, groups, items };
    if (style !== void 0) actions.style = style;
    writeGeneralPatch({ [key]: actions });
  }
});
const tableActionsValue = makeTableActionsValue("actions");
const tableBottomActionsValue = makeTableActionsValue("bottomActions");
const tableQueryValue = computed({
  get: () => {
    const q = editingGeneralConfig.value.query;
    if (q) return q;
    return defaultFormConfig();
  },
  set: (next) => {
    if (!next.fields || next.fields.length === 0) {
      const cur = readGeneral();
      const { query: _drop, ...rest } = cur;
      const merged = normalizeForEmit(rest);
      const cleared = { ...config.value, ...merged };
      delete cleared.query;
      config.value = cleared;
      return;
    }
    writeGeneralPatch({ query: next });
  }
});
</script>

<template>
  <div class="flex flex-col">
    <div class="flex min-h-128 gap-6">
      <div
        v-if="!isDrilled"
        class="flex w-64 shrink-0 flex-col"
      >
        <ScrollArea class="flex-1">
          <div class="flex flex-col gap-1">
            <div
              class="row general-row pl-2"
              :class="[
  selectedGeneral ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              @click.stop="selectGeneral()"
            >
              <Icon
                icon="fluent:settings-20-regular"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">通用配置</span>
            </div>

            <div
              class="row general-row pl-2"
              :class="[
  selectedQuery ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              @click.stop="selectQuery()"
            >
              <Icon
                icon="fluent:form-new-20-regular"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">{{ generalFieldTitle("query") }}</span>
            </div>

            <div
              class="row general-row pl-2"
              :class="[
  selectedActions ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              @click.stop="selectActions()"
            >
              <Icon
                icon="fluent:slide-settings-20-regular"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">{{ generalFieldTitle("actions") }}</span>
            </div>

            <div
              class="row general-row pl-2"
              :class="[
  selectedBottomActions ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              @click.stop="selectBottomActions()"
            >
              <Icon
                icon="fluent:table-bottom-row-20-regular"
                class="size-4 shrink-0"
              />
              <span class="flex-1 truncate">{{ generalFieldTitle("bottomActions") }}</span>
            </div>
          </div>

          <Separator class="my-1" />

          <div
            v-if="pinnedLeftIds.size === 0"
            :ref="dnd.zoneRef(PIN_LEFT_ZONE, pinZoneConfig('left'))"
            class="pin-zone"
            :data-active="dnd.isZoneActive(PIN_LEFT_ZONE) ? 'true' : void 0"
          >
            <Icon
              icon="fluent:pin-20-regular"
              class="size-3.5"
            />
            <span>固定到左侧</span>
          </div>

          <template
            v-for="entry in renderEntries"
            :key="entryKey(entry)"
          >
            <p
              v-if="entry.kind === 'band-caption'"
              class="band-caption"
            >
              {{ entry.text }}
            </p>

            <Separator
              v-else-if="entry.kind === 'band-separator'"
              class="my-1"
            />

            <div
              v-else-if="entry.kind === 'group-header'"
              :ref="dnd.rowRef(entry.groupId, groupRowConfig(entry.groupId))"
              class="row group group-row pl-2"
              :class="[
  selected.kind === 'group' && selected.id === entry.groupId ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : ''
]"
              :data-instruction="dnd.instructionFor(entry.groupId) ?? void 0"
              @click.stop="selectGroup(entry.groupId)"
            >
              <Icon
                icon="fluent:re-order-dots-vertical-20-regular"
                class="drag-handle size-4 shrink-0 cursor-grab text-zinc-400"
              />
              <Icon
                icon="fluent:folder-20-regular"
                class="size-4 shrink-0 text-zinc-400"
              />
              <span class="flex-1 truncate">{{ groupLabel(findGroup(entry.groupId) ?? { id: entry.groupId }) }}</span>
              <Tooltip :delay-duration="180">
                <TooltipTrigger as-child>
                  <button
                    class="shrink-0 text-zinc-300 opacity-0 transition-opacity hover:text-red-500 group-hover:opacity-100"
                    @click.stop="deleteGroup(entry.groupId)"
                  >
                    <Icon
                      icon="fluent:delete-20-regular"
                      class="size-3.5"
                    />
                  </button>
                </TooltipTrigger>
                <TooltipContent side="right">
                  移除该分组（其下的列将变为未分组）
                </TooltipContent>
              </Tooltip>
            </div>

            <div
              v-else-if="entry.kind === 'column'"
              :ref="dnd.rowRef(entry.columnId, columnRowConfig(entry.columnId))"
              class="row group column-row"
              :class="[
  entry.groupId ? 'pl-6' : 'pl-2',
  selected.kind === 'column' && selected.id === entry.columnId ? 'bg-[color-mix(in_srgb,var(--primary)_10%,white)] text-(--primary)' : 'text-zinc-600 hover:bg-zinc-50'
]"
              :data-instruction="dnd.instructionFor(entry.columnId) ?? void 0"
              @click.stop="selectColumn(entry.columnId)"
            >
              <Icon
                icon="fluent:re-order-dots-vertical-20-filled"
                class="drag-handle size-4 shrink-0 cursor-grab text-zinc-300"
              />
              <span class="flex-1 truncate">{{ columnLabel(findColumn(entry.columnId) ?? { id: entry.columnId }) }}</span>
              <Badge
                variant="secondary"
                class="shrink-0 text-[10px] leading-none"
              >
                {{ getColumnTypeLabel(findColumn(entry.columnId)?.type, findColumn(entry.columnId)?.compatibilityDate) }}
              </Badge>
              <Tooltip :delay-duration="180">
                <TooltipTrigger as-child>
                  <button
                    class="shrink-0 text-zinc-300 opacity-0 transition-opacity hover:text-red-500 group-hover:opacity-100"
                    @click.stop="deleteColumn(entry.columnId)"
                  >
                    <Icon
                      icon="fluent:delete-20-regular"
                      class="size-3.5"
                    />
                  </button>
                </TooltipTrigger>
                <TooltipContent side="right">
                  从表格中移除该列
                </TooltipContent>
              </Tooltip>
            </div>
          </template>

          <div
            v-if="pinnedRightIds.size === 0"
            :ref="dnd.zoneRef(PIN_RIGHT_ZONE, pinZoneConfig('right'))"
            class="pin-zone"
            :data-active="dnd.isZoneActive(PIN_RIGHT_ZONE) ? 'true' : void 0"
          >
            <Icon
              icon="fluent:pin-20-regular"
              class="size-3.5"
            />
            <span>固定到右侧</span>
          </div>
        </ScrollArea>

        <div class="flex flex-col gap-2 border-t border-zinc-100 p-2">
          <InputGroup>
            <InputGroupAddon align="inline-start">
              <Icon icon="fluent:add-20-regular" />
            </InputGroupAddon>
            <InputGroupCombobox>
              <CommandGroup>
                <CommandItem
                  v-for="entry in pickerEntries"
                  :key="`${entry.type}@${entry.compatibilityDate}`"
                  :value="`${entry.type}@${entry.compatibilityDate}`"
                  @select="addColumn(entry.type, entry.compatibilityDate)"
                >
                  <Icon
                    :icon="entry.metadata.icon"
                    class="size-4 text-zinc-500"
                  />
                  <span class="flex-1">{{ entry.metadata.name }}</span>
                  <span class="ml-auto text-xs tabular-nums text-zinc-400">{{ entry.compatibilityDate }}</span>
                </CommandItem>
              </CommandGroup>
            </InputGroupCombobox>
          </InputGroup>
        </div>
      </div>

      <div class="flex flex-1 flex-col">
        <ScrollArea class="flex-1">
          <div
            v-if="selectedGeneral"
            class="space-y-5"
          >
            <div class="grid grid-cols-2 gap-x-6 gap-y-4">
              <Field
                v-if="!hideDisplayName"
                orientation="vertical"
              >
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="generalFieldDescription('displayName')"
                    #tooltip
                  >
                    <Markdown
                      :source="generalFieldDescription('displayName')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ generalFieldTitle("displayName") }}
                </FieldLabel>
                <InputGroup>
                  <InputGroupInput
                    :model-value="editingGeneralConfig.displayName ?? ''"
                    placeholder="例：订单列表"
                    @update:model-value="setGeneralDisplayName"
                  />
                </InputGroup>
              </Field>

              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="generalFieldDescription('rowKey')"
                    #tooltip
                  >
                    <Markdown
                      :source="generalFieldDescription('rowKey')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ generalFieldTitle("rowKey") }}
                </FieldLabel>
                <RowKeyCELContext>
                  <ExpressionEditor
                    :model-value="editingGeneralConfig.rowKey ?? ''"
                    placeholder="如 row.id"
                    result-type="string"
                    @update:model-value="(v) => updateGeneralOptionalString('rowKey', v)"
                  />
                </RowKeyCELContext>
              </Field>

              <!-- Host slot inside the general grid: a wrapping editor (e.g. the
                   `table` form field, which hides 内部名称) drops a field into
                   the cell next to 行唯一标识 so the two sit on one row. -->
              <slot name="general-grid-extra" />
            </div>

            <!-- Host extension slot: a wrapping editor (e.g. the `table` form
                 field config) injects its own fields into the general pane. -->
            <slot name="general-extra" />

            <template v-if="!hideDataSource">
              <h3 class="text-xs font-medium text-zinc-500">
                {{ generalFieldTitle("dataSource") }}
              </h3>

              <div class="space-y-4">
                <Field orientation="vertical">
                  <FieldLabel class="text-xs text-zinc-500">
                    <template
                      v-if="dataSourceFieldDescription('request')"
                      #tooltip
                    >
                      <Markdown
                        :source="dataSourceFieldDescription('request')"
                        block
                        class="prose prose-sm prose-zinc"
                      />
                    </template>
                    {{ dataSourceFieldTitle("request") }}
                  </FieldLabel>
                  <RequestCELContext>
                    <ExpressionEditor
                      :model-value="editingGeneralConfig.dataSource?.request ?? ''"
                      placeholder="如 http.get('/api/users').query('page', pageIndex)"
                      result-type="HttpRequest"
                      multiline
                      class="min-h-20"
                      @update:model-value="(v) => updateDataSourceOptional('request', v)"
                    />
                  </RequestCELContext>
                </Field>

                <div class="grid grid-cols-2 gap-x-6 gap-y-4">
                  <Field orientation="vertical">
                    <FieldLabel class="text-xs text-zinc-500">
                      <template
                        v-if="dataSourceFieldDescription('data')"
                        #tooltip
                      >
                        <Markdown
                          :source="dataSourceFieldDescription('data')"
                          block
                          class="prose prose-sm prose-zinc"
                        />
                      </template>
                      {{ dataSourceFieldTitle("data") }}
                    </FieldLabel>
                    <JsonCELContext>
                      <ExpressionEditor
                        :model-value="editingGeneralConfig.dataSource?.data ?? ''"
                        placeholder="如 json.?value.items.orValue([])"
                        result-type="list"
                        @update:model-value="(v) => setDataSourceField('data', v)"
                      />
                    </JsonCELContext>
                  </Field>
                  <Field orientation="vertical">
                    <FieldLabel class="text-xs text-zinc-500">
                      <template
                        v-if="dataSourceFieldDescription('total')"
                        #tooltip
                      >
                        <Markdown
                          :source="dataSourceFieldDescription('total')"
                          block
                          class="prose prose-sm prose-zinc"
                        />
                      </template>
                      {{ dataSourceFieldTitle("total") }}
                    </FieldLabel>
                    <JsonCELContext>
                      <ExpressionEditor
                        :model-value="editingGeneralConfig.dataSource?.total ?? ''"
                        placeholder="留空使用前端分页；如 json.?value.total.orValue(0)"
                        result-type="number"
                        @update:model-value="(v) => updateDataSourceOptional('total', v)"
                      />
                    </JsonCELContext>
                  </Field>
                </div>
              </div>
            </template>

            <Separator />

            <div class="flex items-center gap-2">
              <h3 class="text-xs font-medium text-zinc-500">
                {{ generalFieldTitle("pagination") }}
              </h3>
              <Switch
                size="sm"
                :model-value="editingGeneralConfig.pagination !== void 0"
                aria-label="启用分页"
                @update:model-value="setPaginationEnabled"
              />
            </div>

            <div
              v-if="editingGeneralConfig.pagination !== void 0"
              class="grid grid-cols-2 gap-x-6 gap-y-4"
            >
              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="paginationFieldDescription('pageSize')"
                    #tooltip
                  >
                    <Markdown
                      :source="paginationFieldDescription('pageSize')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ paginationFieldTitle("pageSize") }}
                </FieldLabel>
                <InputGroup>
                  <InputGroupNumberField
                    :model-value="editingGeneralConfig.pagination?.pageSize"
                    :min="1"
                    class="font-mono"
                    @update:model-value="(v) => setPageSize(v)"
                  />
                </InputGroup>
                <p
                  v-if="getError('pagination.pageSize')"
                  class="text-xs text-red-500"
                >
                  {{ getError("pagination.pageSize") }}
                </p>
              </Field>
              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="paginationFieldDescription('pageSizes')"
                    #tooltip
                  >
                    <Markdown
                      :source="paginationFieldDescription('pageSizes')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ paginationFieldTitle("pageSizes") }}
                </FieldLabel>
                <Input
                  :model-value="(editingGeneralConfig.pagination?.pageSizes ?? []).join(', ')"
                  placeholder="如 20, 50, 100, 200"
                  class="font-mono text-xs"
                  @update:model-value="(v) => setPageSizes(String(v))"
                />
                <p
                  v-if="getError('pagination.pageSizes')"
                  class="text-xs text-red-500"
                >
                  {{ getError("pagination.pageSizes") }}
                </p>
              </Field>
              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="paginationFieldDescription('left')"
                    #tooltip
                  >
                    <Markdown
                      :source="paginationFieldDescription('left')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ paginationFieldTitle("left") }}
                </FieldLabel>
                <Locale
                  :model-value="editingGeneralConfig.pagination?.left"
                  markdown
                  @update:model-value="(v) => setPaginationLocale('left', v)"
                />
              </Field>
              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="paginationFieldDescription('right')"
                    #tooltip
                  >
                    <Markdown
                      :source="paginationFieldDescription('right')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ paginationFieldTitle("right") }}
                </FieldLabel>
                <Locale
                  :model-value="editingGeneralConfig.pagination?.right"
                  markdown
                  @update:model-value="(v) => setPaginationLocale('right', v)"
                />
              </Field>
            </div>

            <Separator />

            <div class="flex items-center gap-2">
              <FieldLabel class="text-xs text-zinc-500">
                <template
                  v-if="generalFieldDescription('persistQuery')"
                  #tooltip
                >
                  <Markdown
                    :source="generalFieldDescription('persistQuery')"
                    block
                    class="prose prose-sm prose-zinc"
                  />
                </template>
                {{ generalFieldTitle("persistQuery") }}
              </FieldLabel>
              <Switch
                size="sm"
                :model-value="editingGeneralConfig.persistQuery === true"
                aria-label="保留搜索条件"
                @update:model-value="setPersistQueryEnabled"
              />
            </div>

            <Separator />

            <h3 class="text-xs font-medium text-zinc-500">
              样式
            </h3>

            <div class="space-y-4">
              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="generalFieldDescription('cellStyle')"
                    #tooltip
                  >
                    <Markdown
                      :source="generalFieldDescription('cellStyle')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ generalFieldTitle("cellStyle") }}
                </FieldLabel>
                <CellStyleCELContext>
                  <ExpressionEditor
                    :model-value="editingGeneralConfig.cellStyle ?? ''"
                    placeholder="如 { 'color': 'red' }"
                    result-type="map"
                    multiline
                    class="min-h-10"
                    @update:model-value="(v) => updateGeneralOptionalString('cellStyle', v)"
                  />
                </CellStyleCELContext>
              </Field>

              <Field orientation="vertical">
                <FieldLabel class="text-xs text-zinc-500">
                  <template
                    v-if="generalFieldDescription('style')"
                    #tooltip
                  >
                    <Markdown
                      :source="generalFieldDescription('style')"
                      block
                      class="prose prose-sm prose-zinc"
                    />
                  </template>
                  {{ generalFieldTitle("style") }}
                </FieldLabel>
                <Textarea
                  :model-value="editingGeneralConfig.style ?? ''"
                  placeholder="CSS 样式字符串"
                  class="min-h-20 font-mono text-xs"
                  @update:model-value="(v) => updateGeneralOptionalString('style', String(v))"
                />
              </Field>
            </div>
          </div>

          <TableActionsCELContext v-else-if="selectedActions">
            <ActionsConfigEditor
              v-model="tableActionsValue"
              :configure="configureTableActions"
            />
          </TableActionsCELContext>

          <TableActionsCELContext v-else-if="selectedBottomActions">
            <ActionsConfigEditor
              v-model="tableBottomActionsValue"
              :configure="configureTableActions"
            />
          </TableActionsCELContext>

          <ShwfedFormConfig
            v-else-if="selectedQuery"
            v-model="tableQueryValue"
            :configure="configureTableQuery"
          />

          <div
            v-else-if="selectedGroupRef"
            class="grid grid-cols-2 gap-4"
          >
            <Field orientation="vertical">
              <FieldLabel class="text-xs text-zinc-500">
                <template
                  v-if="groupFieldDescription('title')"
                  #tooltip
                >
                  <Markdown
                    :source="groupFieldDescription('title')"
                    block
                    class="prose prose-sm prose-zinc"
                  />
                </template>
                {{ groupFieldTitle("title") }}
              </FieldLabel>
              <Locale
                :model-value="selectedGroupRef.title"
                @update:model-value="(v) => patchSelectedGroup({ title: v })"
              />
            </Field>

            <Field orientation="vertical">
              <FieldLabel class="text-xs text-zinc-500">
                <template
                  v-if="groupFieldDescription('tooltip')"
                  #tooltip
                >
                  <Markdown
                    :source="groupFieldDescription('tooltip')"
                    block
                    class="prose prose-sm prose-zinc"
                  />
                </template>
                {{ groupFieldTitle("tooltip") }}
              </FieldLabel>
              <Locale
                :model-value="selectedGroupRef.tooltip"
                markdown
                @update:model-value="(v) => patchSelectedGroup({ tooltip: v })"
              />
            </Field>
          </div>

          <div
            v-else-if="selectedColumnRef"
            :class="columnFullPane ? '' : 'space-y-5'"
          >
            <component
              :is="selectedColumnEntry.config"
              v-if="selectedColumnEntry"
              :model-value="selectedColumnRef"
              @update:model-value="(v) => patchSelectedColumn(v)"
            />
            <div
              v-else
              class="text-xs text-red-500"
            >
              未知列类型：{{ selectedColumnRef.type }}@{{ selectedColumnRef.compatibilityDate }}
            </div>
          </div>

          <div
            v-else
            class="flex min-h-112 items-center justify-center text-sm text-zinc-400"
          >
            选择一项以编辑
          </div>
        </ScrollArea>
      </div>
    </div>
  </div>
</template>

<style scoped>
.row{align-items:center;border-radius:.25rem;cursor:pointer;display:flex;font-size:.875rem;gap:.375rem;padding-block:.375rem;padding-right:.5rem;position:relative;transition:background-color .1s ease;-webkit-user-select:none;-moz-user-select:none;user-select:none}.group-row{color:#3f3f46;font-weight:500}.group-row:hover{background:#fafafa}.general-row{font-weight:500}[data-instruction=reorder-above]:before,[data-instruction=reorder-below]:after{background:var(--primary,#2563eb);content:"";height:2px;left:.5rem;pointer-events:none;position:absolute;right:.5rem;z-index:1}[data-instruction=reorder-above]:before{top:-1px}[data-instruction=reorder-below]:after{bottom:-1px}.column-row[data-instruction=make-child],.group-row[data-instruction=make-child]{background:color-mix(in srgb,var(--primary,#2563eb) 12%,#fff);border-radius:.25rem;outline:1px dashed var(--primary,#2563eb);outline-offset:-2px}.band-caption{color:#a1a1aa;font-size:.625rem;font-weight:500;letter-spacing:.04em;padding:.125rem .5rem;text-transform:uppercase}.pin-zone{align-items:center;border-radius:.25rem;color:transparent;display:flex;font-size:.75rem;gap:.25rem;height:.5rem;justify-content:center;margin:.125rem 0;pointer-events:auto;transition:height .12s ease,color .12s ease,background-color .12s ease}.pin-zone>*{pointer-events:none}.pin-zone[data-active=true]{background-color:color-mix(in srgb,var(--primary,#2563eb) 8%,#fff);color:var(--primary,#2563eb);height:1.75rem}
</style>
