refactor grid

This commit is contained in:
samunohito 2024-02-02 09:32:49 +09:00
parent f96c7224a7
commit ff48c77827
14 changed files with 284 additions and 154 deletions

View File

@ -56,10 +56,12 @@ import { useTooltip } from '@/scripts/use-tooltip.js';
import * as os from '@/os.js';
import { CellValue, GridCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;
(ev: 'operation:endEdit', sender: GridCell): void;
(ev: 'operation:validation', sender: GridCell, violation: ValidateViolation): void;
(ev: 'change:value', sender: GridCell, newValue: CellValue): void;
(ev: 'change:contentSize', sender: GridCell, newSize: Size): void;
}>();
@ -210,7 +212,9 @@ function endEditing(applyValue: boolean) {
}
function emitValueChange(newValue: CellValue) {
emit('change:value', cell.value, newValue);
const _cell = cell.value;
const violation = cellValidation(_cell, newValue);
emit('operation:validation', _cell, violation);
}
function emitContentSizeChanged() {

View File

@ -20,10 +20,11 @@
<script setup lang="ts">
import { toRefs } from 'vue';
import { GridEventEmitter, GridRow, GridSetting, Size } from '@/components/grid/grid.js';
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import MkDataCell from '@/components/grid/MkDataCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
import { CellValue, GridCell } from '@/components/grid/cell.js';
import { GridRow } from '@/components/grid/row.js';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;

View File

@ -38,24 +38,17 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue';
import {
ColumnSetting,
DataSource,
GridColumn,
GridEventEmitter,
GridRow,
GridSetting,
GridState,
Size,
} from '@/components/grid/grid.js';
import { DataSource, GridEventEmitter, GridSetting, GridState, Size } from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import { cellValidation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { calcCellWidth, equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, createCell, GridCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/utils.js';
import { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
import { ColumnSetting, createColumn, GridColumn } from '@/components/grid/column.js';
import { createRow, GridRow } from '@/components/grid/row.js';
const props = withDefaults(defineProps<{
gridSetting?: GridSetting,
@ -66,11 +59,15 @@ const props = withDefaults(defineProps<{
rowNumberVisible: true,
}),
});
const { gridSetting, columnSettings, data } = toRefs(props);
const emit = defineEmits<{
(ev: 'event', event: GridEvent, current: GridCurrentState): void;
}>();
// #region Event Definitions
// region Event Definitions
/**
* grid -> 各子コンポーネントのイベント経路を担う{@link GridEventEmitter}おもにpropsでの伝搬が難しいイベントを伝搬するために使用する
* 子コンポーネント -> gridのイベントでは原則使用せず{@link emit}を使用する
@ -87,35 +84,70 @@ const bus = new GridEventEmitter();
*/
const resizeObserver = new ResizeObserver((entries) => setTimeout(() => onResize(entries)));
const { gridSetting, columnSettings, data } = toRefs(props);
const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
const columns = ref<GridColumn[]>([]);
const rows = ref<GridRow[]>([]);
const cells = ref<GridCell[][]>([]);
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
/**
* グリッドの最も上位にある状態
*/
const state = ref<GridState>('normal');
/**
* グリッドの列定義propsで受け取った{@link columnSettings}をもとに{@link refreshColumnsSetting}で再計算される
*/
const columns = ref<GridColumn[]>([]);
/**
* グリッドの行定義propsで受け取った{@link data}をもとに{@link refreshData}で再計算される
*/
const rows = ref<GridRow[]>([]);
/**
* グリッドのセル定義propsで受け取った{@link data}をもとに{@link refreshData}で再計算される
*/
const cells = ref<GridCell[][]>([]);
/**
* mousemoveイベントが発生した際にイベントから取得したセルアドレスを保持するための変数
* セルアドレスが変わった瞬間にイベントを起こしたい時のために前回値として使用する
*/
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
/**
* 編集中のセルのアドレスを保持するための変数
*/
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
/**
* 列の範囲選択をする際の開始地点となるインデックスを保持するための変数
* この開始地点からマウスが動いた地点までの範囲を選択する
*/
const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
/**
* 行の範囲選択をする際の開始地点となるインデックスを保持するための変数
* この開始地点からマウスが動いた地点までの範囲を選択する
*/
const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
/**
* 選択状態のセルを取得するための計算プロパティ選択状態とは{@link GridCell.selected}がtrueのセルのこと
*/
const selectedCell = computed(() => {
const selected = cells.value.flat().filter(it => it.selected);
return selected.length > 0 ? selected[0] : undefined;
});
/**
* 範囲選択状態のセルを取得するための計算プロパティ範囲選択状態とは{@link GridCell.ranged}がtrueのセルのこと
*/
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
/**
* 範囲選択状態のセルの範囲を取得するための計算プロパティ左上のセル番地と右下のセル番地を計算する
*/
const rangedBounds = computed(() => {
const _cells = rangedCells.value;
const cols = _cells.map(it => it.address.col);
const rows = _cells.map(it => it.address.row);
const _cols = _cells.map(it => it.address.col);
const _rows = _cells.map(it => it.address.row);
const leftTop = {
col: Math.min(...cols),
row: Math.min(...rows),
col: Math.min(..._cols),
row: Math.min(..._rows),
};
const rightBottom = {
col: Math.max(...cols),
row: Math.max(...rows),
col: Math.max(..._cols),
row: Math.max(..._rows),
};
return {
@ -123,6 +155,9 @@ const rangedBounds = computed(() => {
rightBottom,
};
});
/**
* グリッドの中で使用可能なセルの範囲を取得するための計算プロパティ左上のセル番地と右下のセル番地を計算する
*/
const availableBounds = computed(() => {
const leftTop = {
col: 0,
@ -134,8 +169,14 @@ const availableBounds = computed(() => {
};
return { leftTop, rightBottom };
});
/**
* 範囲選択状態の行を取得するための計算プロパティ範囲選択状態とは{@link GridRow.ranged}がtrueの行のこと
*/
const rangedRows = computed(() => rows.value.filter(it => it.ranged));
// endregion
// #endregion
watch(columnSettings, refreshColumnsSetting, { immediate: true });
watch(data, refreshData, { immediate: true, deep: true });
@ -145,6 +186,9 @@ if (_DEV_) {
});
}
// #region Event Handlers
// region Event Handlers
function onResize(entries: ResizeObserverEntry[]) {
if (entries.length !== 1 || entries[0].target !== rootEl.value) {
return;
@ -162,7 +206,7 @@ function onResize(entries: ResizeObserverEntry[]) {
state.value = 'normal';
//
unSelectionRange();
unSelectionRangeAll();
// emit
bus.emit('forceRefreshContentSize');
@ -179,6 +223,10 @@ function onResize(entries: ResizeObserverEntry[]) {
}
function onKeyDown(ev: KeyboardEvent) {
function emitKeyEvent() {
emitGridEvent({ type: 'keydown', event: ev });
}
if (_DEV_) {
console.log(`[grid][key] ctrl: ${ev.ctrlKey}, shift: ${ev.shiftKey}, code: ${ev.code}`);
}
@ -225,6 +273,8 @@ function onKeyDown(ev: KeyboardEvent) {
break;
}
default: {
//
emitKeyEvent();
return;
}
}
@ -233,7 +283,7 @@ function onKeyDown(ev: KeyboardEvent) {
expandCellRange(newBounds.leftTop, newBounds.rightBottom);
} else {
//
emitGridEvent({ type: 'keydown', event: ev });
emitKeyEvent();
}
} else {
if (ev.shiftKey) {
@ -311,6 +361,8 @@ function onKeyDown(ev: KeyboardEvent) {
break;
}
default: {
//
emitKeyEvent();
return;
}
}
@ -342,7 +394,7 @@ function onKeyDown(ev: KeyboardEvent) {
}
default: {
//
emitGridEvent({ type: 'keydown', event: ev });
emitKeyEvent();
break;
}
}
@ -387,7 +439,7 @@ function onLeftMouseDown(ev: MouseEvent) {
registerMouseMove();
state.value = 'cellSelecting';
} else if (isColumnHeaderCellAddress(cellAddress)) {
unSelectionRange();
unSelectionRangeAll();
const colCells = cells.value.map(row => row[cellAddress.col]);
selectionRange(...colCells.map(cell => cell.address));
@ -399,7 +451,7 @@ function onLeftMouseDown(ev: MouseEvent) {
rootEl.value?.focus();
} else if (isRowNumberCellAddress(cellAddress)) {
unSelectionRange();
unSelectionRangeAll();
const rowCells = cells.value[cellAddress.row];
selectionRange(...rowCells.map(cell => cell.address));
@ -580,6 +632,11 @@ function onCellEditEnd() {
state.value = 'normal';
}
function onCellValidation(sender: GridCell, violation: ValidateViolation) {
sender.validation = violation;
emitGridEvent({ type: 'cell-validation', violation });
}
function onChangeCellValue(sender: GridCell, newValue: CellValue) {
emitCellValue(sender, newValue);
}
@ -640,6 +697,15 @@ function onHeaderCellWidthLargest(sender: GridColumn) {
}
}
// endregion
// #endregion
// #region Methods
// region Methods
/**
* カラム内のコンテンツを表示しきるために必要な横幅と各セルのコンテンツを表示しきるために必要な横幅を比較し大きい方を列全体の横幅として採用する
*/
function calcLargestCellWidth(column: GridColumn) {
const _cells = cells.value;
const largestColumnWidth = columns.value[column.index].contentSize.width;
@ -660,6 +726,9 @@ function calcLargestCellWidth(column: GridColumn) {
column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
}
/**
* {@link emit}を使用してイベントを発行する
*/
function emitGridEvent(ev: GridEvent) {
const currentState: GridCurrentState = {
selectedCell: selectedCell.value,
@ -680,39 +749,27 @@ function emitGridEvent(ev: GridEvent) {
);
}
/**
* 親コンポーネントに新しい値を通知するセル値のバリデーション結果は問わない親コンポーネント側で制御する
*/
function emitCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
const cellAddress = 'address' in sender ? sender.address : sender;
const cell = cells.value[cellAddress.row][cellAddress.col];
const violation = cellValidation(cell, newValue);
emitGridEvent({ type: 'cell-validation', violation });
cell.validation = {
valid: violation.valid,
violations: violation.violations.filter(it => !it.valid),
};
emitGridEvent({
type: 'cell-value-change',
column: cell.column,
row: cell.row,
violation: cell.validation,
oldValue: cell.value,
newValue: newValue,
});
}
function selectionCell(target: CellAddress) {
if (!availableCellAddress(target)) {
return;
}
unSelectionRange();
const _cells = cells.value;
_cells[target.row][target.col].selected = true;
_cells[target.row][target.col].ranged = true;
}
/**
* {@link selectedCell}のセル番地を取得する
* いずれかのセルが選択されている状態で呼ばれることを想定しているため選択されていない場合は例外を投げる
*/
function requireSelectionCell(): CellAddress {
const selected = selectedCell.value;
if (!selected) {
@ -722,6 +779,25 @@ function requireSelectionCell(): CellAddress {
return selected.address;
}
/**
* {@link target}のセルを選択状態にする
* その際{@link target}以外の行およびセルの範囲選択状態を解除する
*/
function selectionCell(target: CellAddress) {
if (!availableCellAddress(target)) {
return;
}
unSelectionRangeAll();
const _cells = cells.value;
_cells[target.row][target.col].selected = true;
_cells[target.row][target.col].ranged = true;
}
/**
* {@link targets}のセルを範囲選択状態にする
*/
function selectionRange(...targets: CellAddress[]) {
const _cells = cells.value;
for (const target of targets) {
@ -729,7 +805,10 @@ function selectionRange(...targets: CellAddress[]) {
}
}
function unSelectionRange() {
/**
* 行およびセルの範囲選択状態をすべて解除する
*/
function unSelectionRangeAll() {
const _cells = rangedCells.value;
for (const cell of _cells) {
cell.selected = false;
@ -742,6 +821,9 @@ function unSelectionRange() {
}
}
/**
* {@link leftTop}から{@link rightBottom}の範囲外にあるセルを範囲選択状態から外す
*/
function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
const _cells = rangedCells.value;
for (const cell of _cells) {
@ -758,6 +840,9 @@ function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
}
}
/**
* {@link leftTop}から{@link rightBottom}の範囲内にあるセルを範囲選択状態にする
*/
function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
for (const row of targetRows) {
@ -767,6 +852,9 @@ function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
}
}
/**
* {@link top}から{@link bottom}までの行を範囲選択状態にする
*/
function expandRowRange(top: number, bottom: number) {
const targetRows = rows.value.slice(top, bottom + 1);
for (const row of targetRows) {
@ -786,65 +874,6 @@ function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col === -1;
}
function refreshColumnsSetting() {
const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) {
throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
}
refreshData();
}
function refreshData() {
if (_DEV_) {
console.log('[grid][refresh-data]');
}
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => ({
index,
ranged: false,
}));
const _columns: GridColumn[] = columnSettings.value.map((setting, index) => ({
index,
setting,
width: calcCellWidth(setting.width),
contentSize: { width: 0, height: 0 },
}));
const _cells = Array.of<GridCell[]>();
for (const [rowIndex, row] of _rows.entries()) {
const rowCells = Array.of<GridCell>();
for (const [colIndex, column] of _columns.entries()) {
const value = (column.setting.bindTo in _data[rowIndex])
? _data[rowIndex][column.setting.bindTo]
: undefined;
const cell: GridCell = {
address: { col: colIndex, row: rowIndex },
value,
column: column,
row: row,
selected: false,
ranged: false,
contentSize: { width: 0, height: 0 },
validation: {
valid: true,
violations: [],
},
};
rowCells.push(cell);
}
_cells.push(rowCells);
}
rows.value = _rows;
columns.value = _columns;
cells.value = _cells;
}
function registerMouseMove() {
unregisterMouseMove();
addEventListener('mousemove', onMouseMove);
@ -863,6 +892,45 @@ function unregisterMouseUp() {
removeEventListener('mouseup', onMouseUp);
}
function refreshColumnsSetting() {
const bindToList = columnSettings.value.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.value.length) {
//
throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
}
refreshData();
}
function refreshData() {
if (_DEV_) {
console.log('[grid][refresh-data]');
}
const _data: DataSource[] = data.value;
const _rows: GridRow[] = _data.map((_, index) => createRow(index));
const _cols: GridColumn[] = columnSettings.value.map(createColumn);
//
//
const _cells = _rows.map((row, rowIndex) =>
_cols.map(col =>
createCell(
col,
row,
(col.setting.bindTo in _data[rowIndex]) ? _data[rowIndex][col.setting.bindTo] : undefined,
),
),
);
rows.value = _rows;
columns.value = _cols;
cells.value = _cells;
}
// endregion
// #endregion
onMounted(() => {
if (rootEl.value) {
resizeObserver.observe(rootEl.value);

View File

@ -23,7 +23,8 @@
<script setup lang="ts">
import { computed, nextTick, onMounted, onUnmounted, ref, toRefs, watch } from 'vue';
import { GridColumn, GridEventEmitter, Size } from '@/components/grid/grid.js';
import { GridEventEmitter, Size } from '@/components/grid/grid.js';
import { GridColumn } from '@/components/grid/column.js';
const emit = defineEmits<{
(ev: 'operation:beginWidthChange', sender: GridColumn): void;

View File

@ -20,9 +20,10 @@
</template>
<script setup lang="ts">
import { GridColumn, GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
import MkHeaderCell from '@/components/grid/MkHeaderCell.vue';
import MkNumberCell from '@/components/grid/MkNumberCell.vue';
import { GridColumn } from '@/components/grid/column.js';
const emit = defineEmits<{
(ev: 'operation:beginWidthChange', sender: GridColumn): void;

View File

@ -7,7 +7,8 @@
</template>
<script setup lang="ts">
import { GridRow } from '@/components/grid/grid.js';
import { GridRow } from '@/components/grid/row.js';
defineProps<{
content: string,

View File

@ -1,5 +1,6 @@
import { GridColumn, GridRow } from '@/components/grid/grid.js';
import { CellValue, GridCell } from '@/components/grid/cell.js';
import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
export type ValidatorParams = {
column: GridColumn;
@ -14,6 +15,7 @@ export type ValidatorResult = {
export type CellValidator = {
name?: string;
ignoreViolation?: boolean;
validate: (params: ValidatorParams) => ValidatorResult;
}

View File

@ -1,5 +1,7 @@
import { ValidateViolationItem } from '@/components/grid/cell-validators.js';
import { GridColumn, GridRow, Size } from '@/components/grid/grid.js';
import { ValidateViolation, ValidateViolationItem } from '@/components/grid/cell-validators.js';
import { Size } from '@/components/grid/grid.js';
import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
export type CellValue = string | boolean | number | undefined | null
@ -21,9 +23,30 @@ export type GridCell = {
selected: boolean;
ranged: boolean;
contentSize: Size;
validation: {
valid: boolean;
violations: ValidateViolationItem[];
}
validation: ValidateViolation;
}
export function createCell(
column: GridColumn,
row: GridRow,
value: CellValue,
): GridCell {
return {
address: { row: row.index, col: column.index },
value,
column,
row,
selected: false,
ranged: false,
contentSize: { width: 0, height: 0 },
validation: {
valid: true,
params: {
column,
row,
value,
},
violations: [],
},
};
}

View File

@ -0,0 +1,31 @@
import { CellValidator } from '@/components/grid/cell-validators.js';
import { Size, SizeStyle } from '@/components/grid/grid.js';
import { calcCellWidth } from '@/components/grid/utils.js';
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
export type ColumnSetting = {
bindTo: string;
title?: string;
icon?: string;
type: ColumnType;
width: SizeStyle;
editable?: boolean;
validators?: CellValidator[];
};
export type GridColumn = {
index: number;
setting: ColumnSetting;
width: string;
contentSize: Size;
}
export function createColumn(setting: ColumnSetting, index: number): GridColumn {
return {
index,
setting,
width: calcCellWidth(setting.width),
contentSize: { width: 0, height: 0 },
};
}

View File

@ -1,7 +1,9 @@
import { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { GridColumn, GridRow, GridState } from '@/components/grid/grid.js';
import { GridState } from '@/components/grid/grid.js';
import { ValidateViolation } from '@/components/grid/cell-validators.js';
import { MenuItem } from '@/types/menu.js';
import { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
export type GridCurrentState = {
selectedCell?: GridCell;
@ -34,6 +36,7 @@ export type GridCellValueChangeEvent = {
type: 'cell-value-change';
column: GridColumn;
row: GridRow;
violation: ValidateViolation;
oldValue: CellValue;
newValue: CellValue;
};

View File

@ -1,5 +1,4 @@
import { EventEmitter } from 'eventemitter3';
import { CellValidator } from '@/components/grid/cell-validators.js';
import { CellValue } from '@/components/grid/cell.js';
export type GridSetting = {
@ -8,7 +7,15 @@ export type GridSetting = {
export type DataSource = Record<string, CellValue>;
export type GridState = 'normal' | 'cellSelecting' | 'cellEditing' | 'colResizing' | 'colSelecting' | 'rowSelecting' | 'hidden'
export type GridState =
'normal' |
'cellSelecting' |
'cellEditing' |
'colResizing' |
'colSelecting' |
'rowSelecting' |
'hidden'
;
export type Size = {
width: number;
@ -17,30 +24,6 @@ export type Size = {
export type SizeStyle = number | 'auto' | undefined;
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
export type ColumnSetting = {
bindTo: string;
title?: string;
icon?: string;
type: ColumnType;
width: SizeStyle;
editable?: boolean;
validators?: CellValidator[];
};
export type GridColumn = {
index: number;
setting: ColumnSetting;
width: string;
contentSize: Size;
}
export type GridRow = {
index: number;
ranged: boolean;
}
export class GridEventEmitter extends EventEmitter<{
'forceRefreshContentSize': void;
}> {

View File

@ -0,0 +1,11 @@
export type GridRow = {
index: number;
ranged: boolean;
}
export function createRow(index: number): GridRow {
return {
index,
ranged: false,
};
}

View File

@ -34,11 +34,11 @@ import { computed, onMounted, ref, toRefs, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue';
import { required } from '@/components/grid/cell-validators.js';
import MkButton from '@/components/MkButton.vue';
import { ColumnSetting } from '@/components/grid/column.js';
const columnSettings: ColumnSetting[] = [
{ bindTo: 'selected', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },

View File

@ -90,7 +90,6 @@ import { onMounted, ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { fromDriveFile, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { ColumnSetting, GridRow } from '@/components/grid/grid.js';
import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -111,6 +110,8 @@ import {
} from '@/components/grid/grid-event.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { CellValue } from '@/components/grid/cell.js';
import { ColumnSetting } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js';
type FolderItem = {
id?: string;