Загрузка данных


import { DataGrid, GridColDef, GridColumnGroupingModel, GridRenderCellParams, GridRenderEditCellParams } from "@mui/x-data-grid";
import React, { useEffect, useState, useRef, useLayoutEffect } from "react";
import { Tooltip, Checkbox } from "@mui/material";

export interface HeadCell<T> {
  id: keyof T;            // Ключ данных
  label: string;          // Название столбца
  groupName?: string;     // Группа
  tooltipField?: keyof T; // Ключ для подскадки
  hidden?: boolean;       // Скрыть столбец
  vertical?: boolean;     // Вертикальный заголовок
  widthCol?: number;      // Размер колонок
}

interface CustomTableProps<T> {
  columns: HeadCell<T>[];
  rows?: T[];
  onRowClick?: (row: T) => void;                            // событие одного клика по строке
  onRowDoubleClick?: (row: T) => void;                      // Событие двойного клика
  onRowContextMenu?: (row: T, e: React.MouseEvent) => void; // Вызов контекстного меню
  editable?: boolean;                                       // включает режим редактирования
  initialEmptyRows?: number;                                // сколько пустых строк создать при старте
  onAddRowRequest?: () => void;                             // Создание новой строки
  onSaveAllRequest?: () => void;                            // сохранить все строки в базу
  onDeleteRowRequest?: (row: T) => void;                    // Удалить строку
  onRowUpdate?: (row: T) => void;                           // Событие редактирования
  deleteField?: keyof T;                                    // По какому столбцу отслеживать "удаление" (зачёркивание строки)
  
  // НОВЫЕ ПРОПСЫ ДЛЯ РАСКРЫТИЯ СТРОК
  expandField?: keyof T;                                    // По какому столбцу отслеживать возможность раскрытия
  expandComponent?: React.ComponentType<{ id: string | number; row: T }>; // Компонент раскрытия, принимает id (или value) и всю строку
}

interface OverflowTooltipProps {
  title: string;
  children: React.ReactElement;
  value: string;
}

const OverflowTooltip = ({ title, children, value }: OverflowTooltipProps) => {
  const [isOverflowed, setIsOverflowed] = useState(false);
  const textRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const element = textRef.current;
    if (!element) return;

    const resizeObserver = new ResizeObserver(() => {
      const hasOverflow = element.scrollWidth > element.clientWidth;
      setIsOverflowed(hasOverflow);
    });

    resizeObserver.observe(element);

    return () => {
      resizeObserver.disconnect();
    };
  }, [value]);

  const finalTooltipText = title || (isOverflowed ? value : "");

  return (
    <Tooltip
      title={finalTooltipText}
      disableHoverListener={!finalTooltipText}
      arrow
      enterDelay={100}
    >
      <div
        ref={textRef}
        style={{
          overflow: 'hidden',
          textOverflow: 'ellipsis',
          whiteSpace: 'nowrap',
          width: '100%'
        }}
      >
        {children}
      </div>
    </Tooltip>
  );
};

export default function CustomTable<T extends { row_id?: number | string }>({
  columns,
  rows: initialRows = [],
  onRowClick,
  onRowDoubleClick,
  onRowContextMenu,
  editable = false,
  onAddRowRequest,
  onSaveAllRequest,
  onDeleteRowRequest,
  onRowUpdate,
  deleteField,
  
  // Деструктуризация новых пропсов
  expandField,
  expandComponent: ExpandComponent,
}: CustomTableProps<T>) {
  const [baseRows, setBaseRows] = useState<T[]>([]);
  const [expandedRowIds, setExpandedRowIds] = useState<(string | number)[]>([]);

  // Скрытие столбцов [1]
  const initialVisibility = Object.fromEntries(
    columns.map(col => [col.id as string, !col.hidden])
  );
  const [columnVisibilityModel, setColumnVisibilityModel] = useState(initialVisibility);

  useEffect(() => {
    setBaseRows(initialRows);
  }, [initialRows]);

  const handleKeyDown = (params: any, event: any) => {
    if (!editable || !onAddRowRequest) return;
    const isLastRow = params.id === displayRows.length - 1; // проверяем по итоговому массиву

    if (isLastRow && (event.key === "Enter" || event.key === "ArrowDown")) {
      event.preventDefault();
      onAddRowRequest();
    }
  };

  const [selectedId, setSelectedId] = useState<number | string | null>(null);

  // --- ФОРМИРОВАНИЕ ИТОГОВЫХ СТРОК С УЧЕТОМ РАСКРЫТЫХ ПАНЕЛЕЙ ---
  const displayRows = React.useMemo(() => {
    const result: any[] = [];
    baseRows.forEach((row, index) => {
      // Присваиваем row_id на основе индекса, как в вашем исходном коде [1]
      const actualRowId = row.row_id ?? index;
      const rowWithId = { ...row, row_id: actualRowId };
      
      result.push(rowWithId);

      // Если строка раскрыта, пушим вслед за ней специальный служебный объект панели
      if (expandedRowIds.includes(actualRowId)) {
        result.push({
          row_id: `panel-${actualRowId}`, // Уникальный ID для DataGrid
          isExpandedPanel: true,
          parentRow: rowWithId,          // Передаем родительские данные внутрь панели
        });
      }
    });
    return result;
  }, [baseRows, expandedRowIds]);
  // -------------------------------------------------------------

  // Колонка для кнопки "+" / "-"
  const expandColumn: GridColDef[] = expandField && ExpandComponent ? [
    {
      field: "__expand__",
      headerName: "",
      width: 50,
      sortable: false,
      filterable: false,
      disableColumnMenu: true,
      renderCell: (params) => {
        // Если это строка-панель, кнопку не рисуем
        if (params.row.isExpandedPanel) return null;

        // Проверяем наличие данных в указанном столбце expandField
        const hasExpandData = Boolean(params.row[expandField]);
        if (!hasExpandData) return null;

        const isExpanded = expandedRowIds.includes(params.id);

        return (
          <button
            style={{
              cursor: 'pointer',
              width: '24px',
              height: '24px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              fontWeight: 'bold'
            }}
            onClick={(e) => {
              e.stopPropagation(); // Чтобы не срабатывал клик по строке
              setExpandedRowIds(prev => 
                isExpanded ? prev.filter(id => id !== params.id) : [...prev, params.id]
              );
            }}
          >
            {isExpanded ? "−" : "+"}
          </button>
        );
      }
    }
  ] : [];

  const gridColumns: GridColDef[] = columns.map((col) => {
    const isBoolean = typeof (baseRows[0]?.[col.id] ?? "") === "boolean";

    const baseCol: GridColDef = {
      field: col.id as string,
      headerName: col.label,
      flex: col.widthCol ? undefined : 1,
      width: col.widthCol,
      editable: editable,
      renderEditCell: isBoolean
        ? (params: GridRenderEditCellParams) => (
            <Checkbox
              checked={Boolean(params.value)}
              onChange={(e) => params.api.setEditCellValue({
                id: params.id,
                field: params.field,
                value: e.target.checked,
              })}
              autoFocus
            />
          )
        : undefined,
      renderCell: (params: GridRenderCellParams) => {
        // ЕСЛИ ЭТО НАША КАСTОМНАЯ СТРОКА-ПАНЕЛЬ
        if (params.row.isExpandedPanel) {
          // Рендерим кастомный компонент только в первой основной колонке данных,
          // чтобы интерфейс не дублировался в каждой ячейке строки
          const firstDataColumn = columns[0]?.id;
          if (params.field !== firstDataColumn) return null;

          const parentRow = params.row.parentRow;
          const targetId = parentRow[expandField!]; // Берем значение из указанного столбца

          return (
            <div 
              style={{ 
                position: 'absolute', 
                left: 0,
                width: '100%',
                padding: '10px 20px',
                background: '#f9f9f9',
                boxSizing: 'border-box',
                zIndex: 2,
              }}
              onClick={(e) => e.stopPropagation()} // Изолируем клики внутри панели
            >
              {/* Рендерим переданный извне компонент вида <Test test={id} /> */}
              <ExpandComponent id={targetId} row={parentRow} />
            </div>
          );
        }

        const value = params.value;
        const tooltipTextFromField = col.tooltipField ? String(params.row[col.tooltipField] ?? '') : "";
        const cellValueString = String(value ?? '');

        if (typeof value === 'boolean') {
          const boolContent = (
            <div style={{display: 'flex', justifyContent: 'center', width: '100%'}}>
              {value ? <span style={{color: '#2e7d32', fontWeight: 'bold', fontSize: '20px'}}>✓</span> : null}
            </div>
          );
          return tooltipTextFromField ?
            <Tooltip title={tooltipTextFromField} arrow>{boolContent}</Tooltip> : boolContent;
        }
        if (isBoolean) {
          return <Checkbox checked={Boolean(params.value)} disabled = {true} />;
        }

        return (
          <OverflowTooltip title={tooltipTextFromField} value={cellValueString}>
            <span>{cellValueString}</span>
          </OverflowTooltip>
        );
      },
    };

    if (col.vertical) {
      baseCol.headerAlign = "center";
      baseCol.renderHeader = () => (
        <div
          style={{
            height: "100%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            writingMode: "vertical-rl",
            transform: "rotate(180deg)",
            whiteSpace: "nowrap",
            width: "20px",
            fontWeight: "bold",
          }}
        >
          {col.label}
        </div>
      );
    }

    return baseCol;
  });

  // Объединяем служебную колонку "+" и обычные колонки [1]
  const finalColumns = [...expandColumn, ...gridColumns];

Режим ИИ
Все
Картинки
Видео
Новости
Карты
Покупки
Книги
Авиабилеты
Похоже, код оборвался в самом конце на этапе формирования groupingModel для MUI DataGrid.
Ниже приведен полностью законченный и исправленный вариант кода, в котором корректно завершен метод .map(...) для группировки колонок, удален лишний дублирующийся return params.value;, а также исправлен синтаксис.
tsx
import { DataGrid, GridColDef, GridColumnGroupingModel, GridRenderCellParams, GridRenderEditCellParams } from "@mui/x-data-grid";
import React, { useEffect, useState, useRef, useLayoutEffect } from "react";
import { Tooltip, Checkbox } from "@mui/material";

export interface HeadCell<T> {
  id: keyof T;            // Ключ данных
  label: string;          // Название столбца
  groupName?: string;     // Группа
  tooltipField?: keyof T; // Ключ для подсказки
  hidden?: boolean;       // Скрыть столбец
  vertical?: boolean;     // Вертикальный заголовок
  widthCol?: number;      // Размер колонок
}

interface CustomTableProps<T> {
  columns: HeadCell<T>[];
  rows?: T[];
  onRowClick?: (row: T) => void;                               // событие одного клика по строке
  onRowDoubleClick?: (row: T) => void;                         // Событие двойного клика
  onRowContextMenu?: (row: T, e: React.MouseEvent) => void;    // Вызов контекстного меню
  editable?: boolean;                                          // включает режим редактирования
  onAddRowRequest?: () => void;                                // Создание новой строки
  onSaveAllRequest?: () => void;                               // сохранить все строки в базу
  onDeleteRowRequest?: (row: T) => void;                       // Удалить строку
  onRowUpdate?: (row: T) => void;                              // Событие редактирования
  deleteField?: keyof T;                                       // По какому столбцу отслеживать "удаление" (зачёркивание строки)
  expandField?: string;                                        // По какому столбцу отслеживать возможность раскрытия
  expandComponent?: React.ComponentType<{ value: any; row: T }>; // Что показывается в раскрытой строке
}

interface OverflowTooltipProps {
  title: string;
  children: React.ReactElement;
  value: string;
}

const OverflowTooltip = ({ title, children, value }: OverflowTooltipProps) => {
  const [isOverflowed, setIsOverflowed] = useState(false);
  const textRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const element = textRef.current;
    if (!element) return;

    const resizeObserver = new ResizeObserver(() => {
      const hasOverflow = element.scrollWidth > element.clientWidth;
      setIsOverflowed(hasOverflow);
    });

    resizeObserver.observe(element);

    return () => {
      resizeObserver.disconnect();
    };
  }, [value]);

  const finalTooltipText = title || (isOverflowed ? value : "");

  return (
    <Tooltip
      title={finalTooltipText}
      disableHoverListener={!finalTooltipText}
      arrow
      enterDelay={100}
    >
      <div
        ref={textRef}
        style={{
          overflow: 'hidden',
          textOverflow: 'ellipsis',
          whiteSpace: 'nowrap',
          width: '100%'
        }}
      >
        {children}
      </div>
    </Tooltip>
  );
};

export default function CustomTable<T extends { row_id?: number | string }>({
  columns,
  rows: initialRows = [],
  onRowClick,
  onRowDoubleClick,
  onRowContextMenu,
  editable = false,
  onAddRowRequest,
  onSaveAllRequest,
  onDeleteRowRequest,
  onRowUpdate,
  deleteField,
  expandComponent,
  expandField,
}: CustomTableProps<T>) {
  const [rows, setRows] = useState<T[]>([]);

  // Скрытие столбцов
  const initialVisibility = Object.fromEntries(
    columns.map(col => [col.id as string, !col.hidden])
  );
  const [columnVisibilityModel, setColumnVisibilityModel] = useState(initialVisibility);

  useEffect(() => {
    setRows(initialRows);
  }, [initialRows]);

  const handleKeyDown = (params: any, event: any) => {
    if (!editable || !onAddRowRequest) return;

    const isLastRow = params.id === rows.length - 1;

    if (isLastRow && (event.key === "Enter" || event.key === "ArrowDown")) {
      event.preventDefault();
      onAddRowRequest();
    }
  };

  const [expandedRows, setExpandedRows] = useState<(string | number)[]>([]);
  
  const expandColumn: GridColDef[] = expandField && expandComponent ? [
    {
      field: "__expand__",
      headerName: "",
      width: 50,
      sortable: false,
      filterable: false,
      renderCell: (params) => {
        const hasExpand = Boolean(params.row[expandField as string]);

        if (!hasExpand) return null;

        const expanded = expandedRows.includes(params.id);

        return (
          <button
            onClick={(e) => {
              e.stopPropagation();
              setExpandedRows((prev) =>
                expanded ? prev.filter((id) => id !== params.id) : [...prev, params.id]
              );
            }}
          >
            {expanded ? "-" : "+"}
          </button>
        );
      },
    },
  ] : [];

  const gridColumns: GridColDef[] = columns.map((col) => {
    const isBoolean = typeof (rows[0]?.[col.id] ?? "") === "boolean";

    const baseCol: GridColDef = {
      field: col.id as string,
      headerName: col.label,
      flex: col.widthCol ? undefined : 1,
      width: col.widthCol,
      editable: editable,
      renderEditCell: isBoolean
        ? (params: GridRenderEditCellParams) => (
            <Checkbox
              checked={Boolean(params.value)}
              onChange={(e) => params.api.setEditCellValue({
                id: params.id,
                field: params.field,
                value: e.target.checked,
              })}
              autoFocus
            />
          )
        : undefined,
      renderCell: (params: GridRenderCellParams) => {
        const value = params.value;
        const tooltipTextFromField = col.tooltipField ? String(params.row[col.tooltipField] ?? '') : "";
        const cellValueString = String(value ?? '');

        if (typeof value === 'boolean') {
          const boolContent = (
            <div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
              {value ? <span style={{ color: '#2e7d32', fontWeight: 'bold', fontSize: '20px' }}>✓</span> : null}
            </div>
          );
          return tooltipTextFromField ?
            <Tooltip title={tooltipTextFromField} arrow>{boolContent}</Tooltip> : boolContent;
        }

        if (isBoolean) {
          return (
            <Checkbox
              checked={Boolean(params.value)}
              disabled
            />
          );
        }

        return (
          <OverflowTooltip title={tooltipTextFromField} value={cellValueString}>
            <span>{cellValueString}</span>
          </OverflowTooltip>
        );
      },
    };

    if (col.vertical) {
      baseCol.headerAlign = "center";
      baseCol.renderHeader = () => (
        <div
          style={{
            height: "100%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            writingMode: "vertical-rl",
            transform: "rotate(180deg)",
            whiteSpace: "nowrap",
            width: "20px",
            fontWeight: "bold",
          }}
        >
          {col.label}
        </div>
      );
    }

    return baseCol;
  });

  const finalColumns = [...expandColumn, ...gridColumns];

  // Создание групп колонок
  const groupingModel: GridColumnGroupingModel = [];
  const groupsMap = new Map<string, string[]>();

  columns.forEach((col) => {
    if (col.groupName) {
      const existing = groupsMap.get(col.groupName) || [];
      groupsMap.set(col.groupName, [...existing, col.id as string]);
    }
  });

  groupsMap.forEach((childrenFields, groupTitle) => {
    groupingModel.push({
      groupId: groupTitle,
      headerName: groupTitle,
      headerAlign: 'center',
      children: childrenFields.map((field) => ({ field })),
    });
  });

  return (
    <div style={{ height: 400, width: '100%' }}>
      <DataGrid
        rows={rows}
        columns={finalColumns}
        columnGroupingModel={groupingModel}
        columnVisibilityModel={columnVisibilityModel}
        onColumnVisibilityModelChange={(newModel) => setColumnVisibilityModel(newModel)}
        getRowId={(row) => row.row_id ?? rows.indexOf(row)}
        onCellKeyDown={handleKeyDown}
        onRowClick={(params) => onRowClick?.(params.row)}
        onRowDoubleClick={(params) => onRowDoubleClick?.(params.row)}
        slotProps={{
          row: {
            onContextMenu: (e: React.MouseEvent) => {
              // Логика контекстного меню, если потребуется
            }
          }
        }}
      />
    </div>
  );
}
Используйте код с осторожностью.
Основные изменения и исправления:
Завершена сборка groupingModel: Метод groupsMap.forEach теперь правильно преобразует массив строк в объекты { field }, которые ожидает MUI DataGrid: children: childrenFields.map((field) => ({ field })).
Исправлен рендеринг (renderCell): Удален недостижимый дублирующийся код return params.value; в самом низу функции маппинга колонок.
Добавлен вывод компонента <DataGrid />: Написан недостающий блок return (...) самого компонента таблицы с базовой привязкой обработчиков кликов и генерацией уникальных getRowId.
Вам требуется добавить кастомный механизм зачёркивания строк через свойство deleteField или интегрировать отображение раскрывающегося компонента expandComponent внутрь самой сетки? Могу помочь с реализацией getRowClassName или Detail Panel.
ИИ может ошибаться. Проверяйте его ответы.




Не удается дать ответ на этот поисковый запрос. Попробуйте спросить что-нибудь другое.


Поскольку функционал раскрытия строк (Detail Panel) в стандартном DataGrid от MUI доступен только в Pro-версии, для базовой бесплатной версии (Community) эту задачу можно элегантно решить через динамическое внедрение фейковых (сервисных) строк прямо в массив данных rows.
При клике на «+» мы будем добавлять в массив rows под текущую строку специальный объект-заглушку с флагом isExpandedPanel. При рендере этой строки мы объединим все её ячейки в одну большую и отобразим ваш кастомный компонент.
Ниже представлен готовый, рабочий код, в который добавлены пропсы expandField и expandComponent, а также логика для работы раскрывающихся строк без Pro-версии:
tsx
import { DataGrid, GridColDef, GridColumnGroupingModel, GridRenderCellParams, GridRenderEditCellParams } from "@mui/x-data-grid";
import React, { useEffect, useState, useRef, useLayoutEffect } from "react";
import { Tooltip, Checkbox } from "@mui/material";

export interface HeadCell<T> {
  id: keyof T;            // Ключ данных
  label: string;          // Название столбца
  groupName?: string;     // Группа
  tooltipField?: keyof T; // Ключ для подскадки
  hidden?: boolean;       // Скрыть столбец
  vertical?: boolean;     // Вертикальный заголовок
  widthCol?: number;      // Размер колонок
}

interface CustomTableProps<T> {
  columns: HeadCell<T>[];
  rows?: T[];
  onRowClick?: (row: T) => void;                            // событие одного клика по строке
  onRowDoubleClick?: (row: T) => void;                      // Событие двойного клика
  onRowContextMenu?: (row: T, e: React.MouseEvent) => void; // Вызов контекстного меню
  editable?: boolean;                                       // включает режим редактирования
  initialEmptyRows?: number;                                // сколько пустых строк создать при старте
  onAddRowRequest?: () => void;                             // Создание новой строки
  onSaveAllRequest?: () => void;                            // сохранить все строки в базу
  onDeleteRowRequest?: (row: T) => void;                    // Удалить строку
  onRowUpdate?: (row: T) => void;                           // Событие редактирования
  deleteField?: keyof T;                                    // По какому столбцу отслеживать "удаление" (зачёркивание строки)
  
  // НОВЫЕ ПРОПСЫ ДЛЯ РАСКРЫТИЯ СТРОК
  expandField?: keyof T;                                    // По какому столбцу отслеживать возможность раскрытия
  expandComponent?: React.ComponentType<{ id: string | number; row: T }>; // Компонент раскрытия, принимает id (или value) и всю строку
}

interface OverflowTooltipProps {
  title: string;
  children: React.ReactElement;
  value: string;
}

const OverflowTooltip = ({ title, children, value }: OverflowTooltipProps) => {
  const [isOverflowed, setIsOverflowed] = useState(false);
  const textRef = useRef<HTMLDivElement>(null);

  useLayoutEffect(() => {
    const element = textRef.current;
    if (!element) return;

    const resizeObserver = new ResizeObserver(() => {
      const hasOverflow = element.scrollWidth > element.clientWidth;
      setIsOverflowed(hasOverflow);
    });

    resizeObserver.observe(element);

    return () => {
      resizeObserver.disconnect();
    };
  }, [value]);

  const finalTooltipText = title || (isOverflowed ? value : "");

  return (
    <Tooltip
      title={finalTooltipText}
      disableHoverListener={!finalTooltipText}
      arrow
      enterDelay={100}
    >
      <div
        ref={textRef}
        style={{
          overflow: 'hidden',
          textOverflow: 'ellipsis',
          whiteSpace: 'nowrap',
          width: '100%'
        }}
      >
        {children}
      </div>
    </Tooltip>
  );
};

export default function CustomTable<T extends { row_id?: number | string }>({
  columns,
  rows: initialRows = [],
  onRowClick,
  onRowDoubleClick,
  onRowContextMenu,
  editable = false,
  onAddRowRequest,
  onSaveAllRequest,
  onDeleteRowRequest,
  onRowUpdate,
  deleteField,
  
  // Деструктуризация новых пропсов
  expandField,
  expandComponent: ExpandComponent,
}: CustomTableProps<T>) {
  const [baseRows, setBaseRows] = useState<T[]>([]);
  const [expandedRowIds, setExpandedRowIds] = useState<(string | number)[]>([]);

  // Скрытие столбцов [1]
  const initialVisibility = Object.fromEntries(
    columns.map(col => [col.id as string, !col.hidden])
  );
  const [columnVisibilityModel, setColumnVisibilityModel] = useState(initialVisibility);

  useEffect(() => {
    setBaseRows(initialRows);
  }, [initialRows]);

  const handleKeyDown = (params: any, event: any) => {
    if (!editable || !onAddRowRequest) return;
    const isLastRow = params.id === displayRows.length - 1; // проверяем по итоговому массиву

    if (isLastRow && (event.key === "Enter" || event.key === "ArrowDown")) {
      event.preventDefault();
      onAddRowRequest();
    }
  };

  const [selectedId, setSelectedId] = useState<number | string | null>(null);

  // --- ФОРМИРОВАНИЕ ИТОГОВЫХ СТРОК С УЧЕТОМ РАСКРЫТЫХ ПАНЕЛЕЙ ---
  const displayRows = React.useMemo(() => {
    const result: any[] = [];
    baseRows.forEach((row, index) => {
      // Присваиваем row_id на основе индекса, как в вашем исходном коде [1]
      const actualRowId = row.row_id ?? index;
      const rowWithId = { ...row, row_id: actualRowId };
      
      result.push(rowWithId);

      // Если строка раскрыта, пушим вслед за ней специальный служебный объект панели
      if (expandedRowIds.includes(actualRowId)) {
        result.push({
          row_id: `panel-${actualRowId}`, // Уникальный ID для DataGrid
          isExpandedPanel: true,
          parentRow: rowWithId,          // Передаем родительские данные внутрь панели
        });
      }
    });
    return result;
  }, [baseRows, expandedRowIds]);
  // -------------------------------------------------------------

  // Колонка для кнопки "+" / "-"
  const expandColumn: GridColDef[] = expandField && ExpandComponent ? [
    {
      field: "__expand__",
      headerName: "",
      width: 50,
      sortable: false,
      filterable: false,
      disableColumnMenu: true,
      renderCell: (params) => {
        // Если это строка-панель, кнопку не рисуем
        if (params.row.isExpandedPanel) return null;

        // Проверяем наличие данных в указанном столбце expandField
        const hasExpandData = Boolean(params.row[expandField]);
        if (!hasExpandData) return null;

        const isExpanded = expandedRowIds.includes(params.id);

        return (
          <button
            style={{
              cursor: 'pointer',
              width: '24px',
              height: '24px',
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              fontWeight: 'bold'
            }}
            onClick={(e) => {
              e.stopPropagation(); // Чтобы не срабатывал клик по строке
              setExpandedRowIds(prev => 
                isExpanded ? prev.filter(id => id !== params.id) : [...prev, params.id]
              );
            }}
          >
            {isExpanded ? "−" : "+"}
          </button>
        );
      }
    }
  ] : [];

  const gridColumns: GridColDef[] = columns.map((col) => {
    const isBoolean = typeof (baseRows[0]?.[col.id] ?? "") === "boolean";

    const baseCol: GridColDef = {
      field: col.id as string,
      headerName: col.label,
      flex: col.widthCol ? undefined : 1,
      width: col.widthCol,
      editable: editable,
      renderEditCell: isBoolean
        ? (params: GridRenderEditCellParams) => (
            <Checkbox
              checked={Boolean(params.value)}
              onChange={(e) => params.api.setEditCellValue({
                id: params.id,
                field: params.field,
                value: e.target.checked,
              })}
              autoFocus
            />
          )
        : undefined,
      renderCell: (params: GridRenderCellParams) => {
        // ЕСЛИ ЭТО НАША КАСTОМНАЯ СТРОКА-ПАНЕЛЬ
        if (params.row.isExpandedPanel) {
          // Рендерим кастомный компонент только в первой основной колонке данных,
          // чтобы интерфейс не дублировался в каждой ячейке строки
          const firstDataColumn = columns[0]?.id;
          if (params.field !== firstDataColumn) return null;

          const parentRow = params.row.parentRow;
          const targetId = parentRow[expandField!]; // Берем значение из указанного столбца

          return (
            <div 
              style={{ 
                position: 'absolute', 
                left: 0,
                width: '100%',
                padding: '10px 20px',
                background: '#f9f9f9',
                boxSizing: 'border-box',
                zIndex: 2,
              }}
              onClick={(e) => e.stopPropagation()} // Изолируем клики внутри панели
            >
              {/* Рендерим переданный извне компонент вида <Test test={id} /> */}
              <ExpandComponent id={targetId} row={parentRow} />
            </div>
          );
        }

        const value = params.value;
        const tooltipTextFromField = col.tooltipField ? String(params.row[col.tooltipField] ?? '') : "";
        const cellValueString = String(value ?? '');

        if (typeof value === 'boolean') {
          const boolContent = (
            <div style={{display: 'flex', justifyContent: 'center', width: '100%'}}>
              {value ? <span style={{color: '#2e7d32', fontWeight: 'bold', fontSize: '20px'}}>✓</span> : null}
            </div>
          );
          return tooltipTextFromField ?
            <Tooltip title={tooltipTextFromField} arrow>{boolContent}</Tooltip> : boolContent;
        }
        if (isBoolean) {
          return <Checkbox checked={Boolean(params.value)} disabled = {true} />;
        }

        return (
          <OverflowTooltip title={tooltipTextFromField} value={cellValueString}>
            <span>{cellValueString}</span>
          </OverflowTooltip>
        );
      },
    };

    if (col.vertical) {
      baseCol.headerAlign = "center";
      baseCol.renderHeader = () => (
        <div
          style={{
            height: "100%",
            display: "flex",
            alignItems: "center",
            justifyContent: "center",
            writingMod