diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index 6b90af1be3..ce8778af24 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -5,7 +5,7 @@ :style="{ maxWidth: cellWidth, minWidth: cellWidth }" :tabindex="-1" @keydown="onCellKeyDown" - @dblclick="onCellDoubleClick" + @dblclick.prevent="onCellDoubleClick" >
it.className ?? {}), ]" :style="[ - row.additionalStyle?.style ? row.additionalStyle.style : {}, + ...(row.additionalStyles ?? []).map(it => it.style ?? {}), ]" > (); diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index 87e9a46bb3..a799d30578 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -9,7 +9,7 @@ import { computed, onMounted, ref, toRefs, watch } from 'vue'; -import { DataSource, GridEventEmitter, 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'; @@ -49,8 +49,8 @@ import { equalCellAddress, getCellAddress, getCellElement } from '@/components/g import { MenuItem } from '@/types/menu.js'; import * as os from '@/os.js'; import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js'; -import { createColumn, GridColumn, GridColumnSetting } from '@/components/grid/column.js'; -import { createRow, defaultGridSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js'; +import { createColumn, GridColumn } from '@/components/grid/column.js'; +import { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js'; type RowHolder = { row: GridRow, @@ -63,19 +63,18 @@ const emit = defineEmits<{ }>(); const props = defineProps<{ - gridSetting?: GridRowSetting, - columnSettings: GridColumnSetting[], + settings: GridSetting, data: DataSource[] }>(); // non-reactive -const gridSetting: Required = { - ...props.gridSetting, - ...defaultGridSetting, +const rowSetting: Required = { + ...defaultGridRowSetting, + ...props.settings.row, }; // non-reactive -const columnSettings = props.columnSettings; +const columnSettings = props.settings.cols; const { data } = toRefs(props); @@ -434,7 +433,7 @@ function onMouseDown(ev: MouseEvent) { } function onLeftMouseDown(ev: MouseEvent) { - const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); + const cellAddress = getCellAddress(ev.target as HTMLElement, rowSetting); if (_DEV_) { console.log(`[grid][mouse-left] state:${state.value}, button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); } @@ -568,7 +567,7 @@ function onLeftMouseDown(ev: MouseEvent) { } function onRightMouseDown(ev: MouseEvent) { - const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); + const cellAddress = getCellAddress(ev.target as HTMLElement, rowSetting); if (_DEV_) { console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); } @@ -593,7 +592,7 @@ function onRightMouseDown(ev: MouseEvent) { function onMouseMove(ev: MouseEvent) { ev.preventDefault(); - const targetCellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); + const targetCellAddress = getCellAddress(ev.target as HTMLElement, rowSetting); if (equalCellAddress(previousCellAddress.value, targetCellAddress)) { return; } @@ -696,7 +695,7 @@ function onMouseUp(ev: MouseEvent) { } function onContextMenu(ev: MouseEvent) { - const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); + const cellAddress = getCellAddress(ev.target as HTMLElement, rowSetting); if (_DEV_) { console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); } @@ -734,6 +733,7 @@ function onCellEditEnd() { } function onChangeCellValue(sender: GridCell, newValue: CellValue) { + applyRowRules([sender]); emitCellValue(sender, newValue); } @@ -972,7 +972,7 @@ function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) { * {@link top}から{@link bottom}までの行を範囲選択状態にする。 */ function expandRowRange(top: number, bottom: number) { - if (!gridSetting.selectable) { + if (!rowSetting.selectable) { return; } @@ -982,6 +982,31 @@ function expandRowRange(top: number, bottom: number) { } } +function applyRowRules(targetCells: GridCell[]) { + const _rows = rows.value; + const targetRowIdxes = [...new Set(targetCells.map(it => it.address.row))]; + const rowGroups = Array.of<{ row: GridRow, cells: GridCell[] }>(); + for (const rowIdx of targetRowIdxes) { + const rowGroup = targetCells.filter(it => it.address.row === rowIdx); + rowGroups.push({ row: _rows[rowIdx], cells: rowGroup }); + } + + const _cells = cells.value; + for (const group of rowGroups.filter(it => it.row.using)) { + const row = group.row; + const targetCols = group.cells.map(it => it.column); + const cells = _cells[group.row.index].cells; + + const newStyles = rowSetting.styleRules + .filter(it => it.condition({ row, targetCols, cells })) + .map(it => it.applyStyle); + + if (JSON.stringify(newStyles) !== JSON.stringify(row.additionalStyles)) { + row.additionalStyles = newStyles; + } + } +} + function availableCellAddress(cellAddress: CellAddress): boolean { return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length; } @@ -1018,9 +1043,9 @@ function refreshData() { } const _data: DataSource[] = data.value; - const _rows: GridRow[] = (_data.length > gridSetting.minimumDefinitionCount) + const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount) ? _data.map((_, index) => createRow(index, true)) - : Array.from({ length: gridSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length)); + : Array.from({ length: rowSetting.minimumDefinitionCount }, (_, index) => createRow(index, index < _data.length)); const _cols: GridColumn[] = columns.value; // 行・列の定義から、元データの配列より値を取得してセルを作成する。 @@ -1028,11 +1053,11 @@ function refreshData() { const _cells: RowHolder[] = _rows.map(row => { const cells = row.using ? _cols.map(col => { - const cell = createCell(col, row, _data[row.index][col.setting.bindTo], col.setting.cellSetting ?? {}); + const cell = createCell(col, row, _data[row.index][col.setting.bindTo]); cell.violation = cellValidation(cell, cell.value); return cell; }) - : _cols.map(col => createCell(col, row, undefined, col.setting.cellSetting ?? {})); + : _cols.map(col => createCell(col, row, undefined)); return { row, cells, origin: _data[row.index] }; }); @@ -1040,6 +1065,8 @@ function refreshData() { rows.value = _rows; cells.value = _cells; + applyRowRules(_cells.filter(it => it.row.using).flatMap(it => it.cells)); + if (_DEV_) { console.log('[grid][refresh-data][end]'); } @@ -1057,7 +1084,7 @@ function refreshData() { */ function patchData(newItems: DataSource[]) { if (_DEV_) { - console.log(`[grid][patch-data][begin] new:${newItems.length} old:${cells.value.length}`); + console.log('[grid][patch-data][begin]'); } const _cols = columns.value; @@ -1079,13 +1106,16 @@ function patchData(newItems: DataSource[]) { rows.value.push(...newRows); cells.value.push(...newCells); + + applyRowRules(newCells.flatMap(it => it.cells)); } // 行数の上限が欲しい場合はここに設けてもいいかもしれない - if (rows.value.length > newItems.length) { + const usingRows = rows.value.filter(it => it.using); + if (usingRows.length > newItems.length) { // 行数が減っているので古い行をクリアする(再マウント・再レンダリングが重いので要素そのものは消さない) - for (let rowIdx = newItems.length; rowIdx < rows.value.length; rowIdx++) { + for (let rowIdx = newItems.length; rowIdx < usingRows.length; rowIdx++) { resetRow(rows.value[rowIdx]); for (let colIdx = 0; colIdx < _cols.length; colIdx++) { const holder = cells.value[rowIdx]; @@ -1095,6 +1125,7 @@ function patchData(newItems: DataSource[]) { } } + const changedCells = Array.of(); for (let rowIdx = 0; rowIdx < newItems.length; rowIdx++) { const holder = cells.value[rowIdx]; holder.row.using = true; @@ -1109,19 +1140,24 @@ function patchData(newItems: DataSource[]) { if (oldCell.value !== newValue) { oldCell.violation = cellValidation(oldCell, newValue); oldCell.value = newValue; + changedCells.push(oldCell); } } } - // セル値が書き換わっており、バリデーションの結果も変わっているので外部に通知する必要がある - emitGridEvent({ - type: 'cell-validation', - all: cells.value - .filter(it => it.row.using) - .flatMap(it => it.cells) - .map(it => it.violation) - .filter(it => !it.valid), - }); + if (changedCells.length > 0) { + applyRowRules(changedCells); + + // セル値が書き換わっており、バリデーションの結果も変わっているので外部に通知する必要がある + emitGridEvent({ + type: 'cell-validation', + all: cells.value + .filter(it => it.row.using) + .flatMap(it => it.cells) + .map(it => it.violation) + .filter(it => !it.valid), + }); + } if (_DEV_) { console.log('[grid][patch-data][end]'); diff --git a/packages/frontend/src/components/grid/cell.ts b/packages/frontend/src/components/grid/cell.ts index 95594b6005..bb7c006433 100644 --- a/packages/frontend/src/components/grid/cell.ts +++ b/packages/frontend/src/components/grid/cell.ts @@ -1,8 +1,7 @@ import { ValidateViolation } from '@/components/grid/cell-validators.js'; -import { AdditionalStyle, EventOptions, Size } from '@/components/grid/grid.js'; +import { Size } from '@/components/grid/grid.js'; import { GridColumn } from '@/components/grid/column.js'; import { GridRow } from '@/components/grid/row.js'; -import { MenuItem } from '@/types/menu.js'; export type CellValue = string | boolean | number | undefined | null diff --git a/packages/frontend/src/components/grid/column.ts b/packages/frontend/src/components/grid/column.ts index 07ddf3ef57..42f855e17c 100644 --- a/packages/frontend/src/components/grid/column.ts +++ b/packages/frontend/src/components/grid/column.ts @@ -1,10 +1,8 @@ import { CellValidator } from '@/components/grid/cell-validators.js'; -import { AdditionalStyle, EventOptions, Size, SizeStyle } from '@/components/grid/grid.js'; +import { Size, SizeStyle } from '@/components/grid/grid.js'; import { calcCellWidth } from '@/components/grid/grid-utils.js'; -import { CellValue, GridCell, GridCellSetting } from '@/components/grid/cell.js'; -import { GridRow } from '@/components/grid/row.js'; -export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image'; +export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden'; export type GridColumnSetting = { bindTo: string; @@ -14,8 +12,6 @@ export type GridColumnSetting = { width: SizeStyle; editable?: boolean; validators?: CellValidator[]; - valueConverter?: GridColumnValueConverter; - cellSetting?: GridCellSetting; }; export type GridColumn = { @@ -25,13 +21,6 @@ export type GridColumn = { contentSize: Size; } -export type GridColumnValueConverter = (row: GridRow, col: GridColumn, value: CellValue) => CellValue; - -export type GridColumnEventArgs = { - col: GridColumn; - cells: GridCell[]; -} & EventOptions; - export function createColumn(setting: GridColumnSetting, index: number): GridColumn { return { index, diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts index 5ffb0a2cb3..41a092f67e 100644 --- a/packages/frontend/src/components/grid/grid.ts +++ b/packages/frontend/src/components/grid/grid.ts @@ -4,7 +4,7 @@ import { GridColumnSetting } from '@/components/grid/column.js'; import { GridRowSetting } from '@/components/grid/row.js'; export type GridSetting = { - row: GridRowSetting; + row?: GridRowSetting; cols: GridColumnSetting[]; }; @@ -27,11 +27,6 @@ export type Size = { export type SizeStyle = number | 'auto' | undefined; -export type EventOptions = { - preventDefault?: boolean; - stopPropagation?: boolean; -} - export type AdditionalStyle = { className?: string; style?: Record; diff --git a/packages/frontend/src/components/grid/row.ts b/packages/frontend/src/components/grid/row.ts index 50e78bcf59..32ae31c43d 100644 --- a/packages/frontend/src/components/grid/row.ts +++ b/packages/frontend/src/components/grid/row.ts @@ -1,22 +1,37 @@ import { AdditionalStyle } from '@/components/grid/grid.js'; +import { GridCell } from '@/components/grid/cell.js'; +import { GridColumn } from '@/components/grid/column.js'; -export const defaultGridSetting: Required = { +export const defaultGridRowSetting: Required = { showNumber: true, selectable: true, minimumDefinitionCount: 100, + styleRules: [], }; +export type GridRowStyleRuleConditionParams = { + row: GridRow, + targetCols: GridColumn[], + cells: GridCell[] +}; + +export type GridRowStyleRule = { + condition: (params: GridRowStyleRuleConditionParams) => boolean; + applyStyle: AdditionalStyle; +} + export type GridRowSetting = { showNumber?: boolean; selectable?: boolean; minimumDefinitionCount?: number; + styleRules?: GridRowStyleRule[]; } export type GridRow = { index: number; ranged: boolean; using: boolean; - additionalStyle?: AdditionalStyle; + additionalStyles: AdditionalStyle[]; } export function createRow(index: number, using: boolean): GridRow { @@ -24,12 +39,13 @@ export function createRow(index: number, using: boolean): GridRow { index, ranged: false, using: using, + additionalStyles: [], }; } export function resetRow(row: GridRow): void { row.ranged = false; row.using = false; - row.additionalStyle = undefined; + row.additionalStyles = []; } diff --git a/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue index b0c4965d6d..3c7d77e89a 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-grid.local.list.vue @@ -79,7 +79,7 @@
- +
@@ -113,7 +113,6 @@ import MkGrid from '@/components/grid/MkGrid.vue'; import { i18n } from '@/i18n.js'; import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; -import { GridColumnSetting } from '@/components/grid/column.js'; import { validators } from '@/components/grid/cell-validators.js'; import { GridCellContextMenuEvent, @@ -131,7 +130,7 @@ import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkSelect from '@/components/MkSelect.vue'; import { deviceKind } from '@/scripts/device-kind.js'; -import { GridRowSetting } from '@/components/grid/row.js'; +import { GridSetting } from '@/components/grid/grid.js'; type GridItem = { checked: boolean; @@ -146,26 +145,42 @@ type GridItem = { localOnly: boolean; roleIdsThatCanBeUsedThisEmojiAsReaction: string; fileId?: string; + updatedAt: string | null; } -const gridSetting: GridRowSetting = { - showNumber: true, - selectable: false, -}; - -const required = validators.required(); -const regex = validators.regex(/^[a-zA-Z0-9_]+$/); -const columnSettings: GridColumnSetting[] = [ - { bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, - { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] }, - { bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required, regex] }, - { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, - { bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 }, - { bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 }, - { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 }, - { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, - { bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 }, -]; +function setupGrid(): GridSetting { + const required = validators.required(); + const regex = validators.regex(/^[a-zA-Z0-9_]+$/); + return { + row: { + showNumber: true, + selectable: true, + minimumDefinitionCount: 100, + styleRules: [ + { + condition: ({ row }) => JSON.stringify(gridItems.value[row.index]) !== JSON.stringify(originGridItems.value[row.index]), + applyStyle: { className: 'changedRow' }, + }, + { + condition: ({ cells }) => cells.some(it => !it.violation.valid), + applyStyle: { className: 'violationRow' }, + }, + ], + }, + cols: [ + { bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, + { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required] }, + { bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required, regex] }, + { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, + { bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 }, + { bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 }, + { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 }, + { bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, + { bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 }, + { bindTo: 'updatedAt', type: 'hidden', editable: false, width: 'auto' }, + ], + }; +} const customEmojis = ref([]); const allPages = ref(0); @@ -229,7 +244,7 @@ async function onUpdateButtonClicked() { isSensitive: item.isSensitive, localOnly: item.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction), - fileId: item.fileId, + fileId: item.fileId, }) .then(() => ({ item, success: true, err: undefined })) .catch(err => ({ item, success: false, err })), @@ -399,15 +414,6 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) { } else { gridItems.value[row.index][column.setting.bindTo] = newValue; } - - const originItem = originGridItems.value[row.index][column.setting.bindTo]; - if (originItem !== newValue) { - row.additionalStyle = { - className: 'editedRow', - }; - } else { - row.additionalStyle = undefined; - } } } @@ -446,14 +452,6 @@ async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentS for (const cell of ranges) { if (cell.column.setting.editable) { gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined; - const originItem = originGridItems.value[cell.row.index][cell.column.setting.bindTo]; - if (originItem !== undefined) { - cell.row.additionalStyle = { - className: 'editedRow', - }; - } else { - cell.row.additionalStyle = undefined; - } } } } @@ -519,6 +517,7 @@ function refreshGridItems() { isSensitive: it.isSensitive, localOnly: it.localOnly, roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '), + updatedAt: it.updatedAt, })); originGridItems.value = JSON.parse(JSON.stringify(gridItems.value)); } @@ -529,6 +528,16 @@ onMounted(async () => { + + +