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 ?? {}),
]"
>
emit('operation:beginEdit', sender)"
@operation:endEdit="(sender) => emit('operation:endEdit', sender)"
@@ -44,7 +45,7 @@ const emit = defineEmits<{
defineProps<{
row: GridRow,
cells: GridCell[],
- gridSetting: GridRowSetting,
+ setting: GridRowSetting,
bus: GridEventEmitter,
}>();
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 () => {
+
+
+