support image change and highlight row

This commit is contained in:
samunohito 2024-02-10 22:31:04 +09:00
parent 171b596ac7
commit 83228a3422
11 changed files with 216 additions and 140 deletions

View File

@ -5,7 +5,7 @@
:style="{ maxWidth: cellWidth, minWidth: cellWidth }" :style="{ maxWidth: cellWidth, minWidth: cellWidth }"
:tabindex="-1" :tabindex="-1"
@keydown="onCellKeyDown" @keydown="onCellKeyDown"
@dblclick="onCellDoubleClick" @dblclick.prevent="onCellDoubleClick"
> >
<div <div
:class="[ :class="[
@ -69,11 +69,11 @@ const emit = defineEmits<{
}>(); }>();
const props = defineProps<{ const props = defineProps<{
cell: GridCell, cell: GridCell,
gridSetting: GridRowSetting, rowSetting: GridRowSetting,
bus: GridEventEmitter, bus: GridEventEmitter,
}>(); }>();
const { cell, gridSetting, bus } = toRefs(props); const { cell, rowSetting, bus } = toRefs(props);
const rootEl = shallowRef<InstanceType<typeof HTMLTableCellElement>>(); const rootEl = shallowRef<InstanceType<typeof HTMLTableCellElement>>();
const contentAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>(); const contentAreaEl = shallowRef<InstanceType<typeof HTMLDivElement>>();
@ -115,7 +115,7 @@ function onCellDoubleClick(ev: MouseEvent) {
function onOutsideMouseDown(ev: MouseEvent) { function onOutsideMouseDown(ev: MouseEvent) {
const isOutside = ev.target instanceof Node && !rootEl.value?.contains(ev.target); const isOutside = ev.target instanceof Node && !rootEl.value?.contains(ev.target);
if (isOutside || !equalCellAddress(cell.value.address, getCellAddress(ev.target as HTMLElement, gridSetting.value))) { if (isOutside || !equalCellAddress(cell.value.address, getCellAddress(ev.target as HTMLElement, rowSetting.value))) {
endEditing(true); endEditing(true);
} }
} }
@ -207,7 +207,7 @@ function endEditing(applyValue: boolean) {
emit('operation:endEdit', cell.value); emit('operation:endEdit', cell.value);
unregisterOutsideMouseDown(); unregisterOutsideMouseDown();
if (applyValue) { if (applyValue && editingValue.value !== cell.value.value) {
emitValueChange(editingValue.value); emitValueChange(editingValue.value);
} }

View File

@ -3,22 +3,23 @@
:class="[ :class="[
$style.row, $style.row,
row.ranged ? $style.ranged : {}, row.ranged ? $style.ranged : {},
row.additionalStyle?.className ? row.additionalStyle.className : {}, ...(row.additionalStyles ?? []).map(it => it.className ?? {}),
]" ]"
:style="[ :style="[
row.additionalStyle?.style ? row.additionalStyle.style : {}, ...(row.additionalStyles ?? []).map(it => it.style ?? {}),
]" ]"
> >
<MkNumberCell <MkNumberCell
v-if="gridSetting.showNumber" v-if="setting.showNumber"
:content="(row.index + 1).toString()" :content="(row.index + 1).toString()"
:row="row" :row="row"
/> />
<MkDataCell <MkDataCell
v-for="cell in cells" v-for="cell in cells"
:key="cell.address.col" :key="cell.address.col"
:vIf="cell.column.setting.type !== 'hidden'"
:cell="cell" :cell="cell"
:gridSetting="gridSetting" :rowSetting="setting"
:bus="bus" :bus="bus"
@operation:beginEdit="(sender) => emit('operation:beginEdit', sender)" @operation:beginEdit="(sender) => emit('operation:beginEdit', sender)"
@operation:endEdit="(sender) => emit('operation:endEdit', sender)" @operation:endEdit="(sender) => emit('operation:endEdit', sender)"
@ -44,7 +45,7 @@ const emit = defineEmits<{
defineProps<{ defineProps<{
row: GridRow, row: GridRow,
cells: GridCell[], cells: GridCell[],
gridSetting: GridRowSetting, setting: GridRowSetting,
bus: GridEventEmitter, bus: GridEventEmitter,
}>(); }>();

View File

@ -9,7 +9,7 @@
<thead> <thead>
<MkHeaderRow <MkHeaderRow
:columns="columns" :columns="columns"
:gridSetting="gridSetting" :gridSetting="rowSetting"
:bus="bus" :bus="bus"
@operation:beginWidthChange="onHeaderCellWidthBeginChange" @operation:beginWidthChange="onHeaderCellWidthBeginChange"
@operation:endWidthChange="onHeaderCellWidthEndChange" @operation:endWidthChange="onHeaderCellWidthEndChange"
@ -25,7 +25,7 @@
:key="row.index" :key="row.index"
:row="row" :row="row"
:cells="cells[row.index].cells" :cells="cells[row.index].cells"
:gridSetting="gridSetting" :setting="rowSetting"
:bus="bus" :bus="bus"
:using="row.using" :using="row.using"
:class="[ lastLine === row.index ? $style.lastLine : {} ]" :class="[ lastLine === row.index ? $style.lastLine : {} ]"
@ -40,7 +40,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, toRefs, watch } from 'vue'; 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 MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue'; import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import { cellValidation } from '@/components/grid/cell-validators.js'; 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 { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js'; import { GridCurrentState, GridEvent } from '@/components/grid/grid-event.js';
import { createColumn, GridColumn, GridColumnSetting } from '@/components/grid/column.js'; import { createColumn, GridColumn } from '@/components/grid/column.js';
import { createRow, defaultGridSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js'; import { createRow, defaultGridRowSetting, GridRow, GridRowSetting, resetRow } from '@/components/grid/row.js';
type RowHolder = { type RowHolder = {
row: GridRow, row: GridRow,
@ -63,19 +63,18 @@ const emit = defineEmits<{
}>(); }>();
const props = defineProps<{ const props = defineProps<{
gridSetting?: GridRowSetting, settings: GridSetting,
columnSettings: GridColumnSetting[],
data: DataSource[] data: DataSource[]
}>(); }>();
// non-reactive // non-reactive
const gridSetting: Required<GridRowSetting> = { const rowSetting: Required<GridRowSetting> = {
...props.gridSetting, ...defaultGridRowSetting,
...defaultGridSetting, ...props.settings.row,
}; };
// non-reactive // non-reactive
const columnSettings = props.columnSettings; const columnSettings = props.settings.cols;
const { data } = toRefs(props); const { data } = toRefs(props);
@ -434,7 +433,7 @@ function onMouseDown(ev: MouseEvent) {
} }
function onLeftMouseDown(ev: MouseEvent) { function onLeftMouseDown(ev: MouseEvent) {
const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); const cellAddress = getCellAddress(ev.target as HTMLElement, rowSetting);
if (_DEV_) { if (_DEV_) {
console.log(`[grid][mouse-left] state:${state.value}, button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); 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) { function onRightMouseDown(ev: MouseEvent) {
const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); const cellAddress = getCellAddress(ev.target as HTMLElement, rowSetting);
if (_DEV_) { if (_DEV_) {
console.log(`[grid][mouse-right] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); 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) { function onMouseMove(ev: MouseEvent) {
ev.preventDefault(); ev.preventDefault();
const targetCellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); const targetCellAddress = getCellAddress(ev.target as HTMLElement, rowSetting);
if (equalCellAddress(previousCellAddress.value, targetCellAddress)) { if (equalCellAddress(previousCellAddress.value, targetCellAddress)) {
return; return;
} }
@ -696,7 +695,7 @@ function onMouseUp(ev: MouseEvent) {
} }
function onContextMenu(ev: MouseEvent) { function onContextMenu(ev: MouseEvent) {
const cellAddress = getCellAddress(ev.target as HTMLElement, gridSetting); const cellAddress = getCellAddress(ev.target as HTMLElement, rowSetting);
if (_DEV_) { if (_DEV_) {
console.log(`[grid][context-menu] button: ${ev.button}, cell: ${cellAddress.row}x${cellAddress.col}`); 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) { function onChangeCellValue(sender: GridCell, newValue: CellValue) {
applyRowRules([sender]);
emitCellValue(sender, newValue); emitCellValue(sender, newValue);
} }
@ -972,7 +972,7 @@ function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
* {@link top}から{@link bottom}までの行を範囲選択状態にする * {@link top}から{@link bottom}までの行を範囲選択状態にする
*/ */
function expandRowRange(top: number, bottom: number) { function expandRowRange(top: number, bottom: number) {
if (!gridSetting.selectable) { if (!rowSetting.selectable) {
return; 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 { function availableCellAddress(cellAddress: CellAddress): boolean {
return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length; 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 _data: DataSource[] = data.value;
const _rows: GridRow[] = (_data.length > gridSetting.minimumDefinitionCount) const _rows: GridRow[] = (_data.length > rowSetting.minimumDefinitionCount)
? _data.map((_, index) => createRow(index, true)) ? _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; const _cols: GridColumn[] = columns.value;
// //
@ -1028,11 +1053,11 @@ function refreshData() {
const _cells: RowHolder[] = _rows.map(row => { const _cells: RowHolder[] = _rows.map(row => {
const cells = row.using const cells = row.using
? _cols.map(col => { ? _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); cell.violation = cellValidation(cell, cell.value);
return cell; 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] }; return { row, cells, origin: _data[row.index] };
}); });
@ -1040,6 +1065,8 @@ function refreshData() {
rows.value = _rows; rows.value = _rows;
cells.value = _cells; cells.value = _cells;
applyRowRules(_cells.filter(it => it.row.using).flatMap(it => it.cells));
if (_DEV_) { if (_DEV_) {
console.log('[grid][refresh-data][end]'); console.log('[grid][refresh-data][end]');
} }
@ -1057,7 +1084,7 @@ function refreshData() {
*/ */
function patchData(newItems: DataSource[]) { function patchData(newItems: DataSource[]) {
if (_DEV_) { 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; const _cols = columns.value;
@ -1079,13 +1106,16 @@ function patchData(newItems: DataSource[]) {
rows.value.push(...newRows); rows.value.push(...newRows);
cells.value.push(...newCells); 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]); resetRow(rows.value[rowIdx]);
for (let colIdx = 0; colIdx < _cols.length; colIdx++) { for (let colIdx = 0; colIdx < _cols.length; colIdx++) {
const holder = cells.value[rowIdx]; const holder = cells.value[rowIdx];
@ -1095,6 +1125,7 @@ function patchData(newItems: DataSource[]) {
} }
} }
const changedCells = Array.of<GridCell>();
for (let rowIdx = 0; rowIdx < newItems.length; rowIdx++) { for (let rowIdx = 0; rowIdx < newItems.length; rowIdx++) {
const holder = cells.value[rowIdx]; const holder = cells.value[rowIdx];
holder.row.using = true; holder.row.using = true;
@ -1109,19 +1140,24 @@ function patchData(newItems: DataSource[]) {
if (oldCell.value !== newValue) { if (oldCell.value !== newValue) {
oldCell.violation = cellValidation(oldCell, newValue); oldCell.violation = cellValidation(oldCell, newValue);
oldCell.value = newValue; oldCell.value = newValue;
changedCells.push(oldCell);
} }
} }
} }
// if (changedCells.length > 0) {
emitGridEvent({ applyRowRules(changedCells);
type: 'cell-validation',
all: cells.value //
.filter(it => it.row.using) emitGridEvent({
.flatMap(it => it.cells) type: 'cell-validation',
.map(it => it.violation) all: cells.value
.filter(it => !it.valid), .filter(it => it.row.using)
}); .flatMap(it => it.cells)
.map(it => it.violation)
.filter(it => !it.valid),
});
}
if (_DEV_) { if (_DEV_) {
console.log('[grid][patch-data][end]'); console.log('[grid][patch-data][end]');

View File

@ -1,8 +1,7 @@
import { ValidateViolation } from '@/components/grid/cell-validators.js'; 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 { GridColumn } from '@/components/grid/column.js';
import { GridRow } from '@/components/grid/row.js'; import { GridRow } from '@/components/grid/row.js';
import { MenuItem } from '@/types/menu.js';
export type CellValue = string | boolean | number | undefined | null export type CellValue = string | boolean | number | undefined | null

View File

@ -1,10 +1,8 @@
import { CellValidator } from '@/components/grid/cell-validators.js'; 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 { 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 = { export type GridColumnSetting = {
bindTo: string; bindTo: string;
@ -14,8 +12,6 @@ export type GridColumnSetting = {
width: SizeStyle; width: SizeStyle;
editable?: boolean; editable?: boolean;
validators?: CellValidator[]; validators?: CellValidator[];
valueConverter?: GridColumnValueConverter;
cellSetting?: GridCellSetting;
}; };
export type GridColumn = { export type GridColumn = {
@ -25,13 +21,6 @@ export type GridColumn = {
contentSize: Size; 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 { export function createColumn(setting: GridColumnSetting, index: number): GridColumn {
return { return {
index, index,

View File

@ -4,7 +4,7 @@ import { GridColumnSetting } from '@/components/grid/column.js';
import { GridRowSetting } from '@/components/grid/row.js'; import { GridRowSetting } from '@/components/grid/row.js';
export type GridSetting = { export type GridSetting = {
row: GridRowSetting; row?: GridRowSetting;
cols: GridColumnSetting[]; cols: GridColumnSetting[];
}; };
@ -27,11 +27,6 @@ export type Size = {
export type SizeStyle = number | 'auto' | undefined; export type SizeStyle = number | 'auto' | undefined;
export type EventOptions = {
preventDefault?: boolean;
stopPropagation?: boolean;
}
export type AdditionalStyle = { export type AdditionalStyle = {
className?: string; className?: string;
style?: Record<string, string | number>; style?: Record<string, string | number>;

View File

@ -1,22 +1,37 @@
import { AdditionalStyle } from '@/components/grid/grid.js'; 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<GridRowSetting> = { export const defaultGridRowSetting: Required<GridRowSetting> = {
showNumber: true, showNumber: true,
selectable: true, selectable: true,
minimumDefinitionCount: 100, minimumDefinitionCount: 100,
styleRules: [],
}; };
export type GridRowStyleRuleConditionParams = {
row: GridRow,
targetCols: GridColumn[],
cells: GridCell[]
};
export type GridRowStyleRule = {
condition: (params: GridRowStyleRuleConditionParams) => boolean;
applyStyle: AdditionalStyle;
}
export type GridRowSetting = { export type GridRowSetting = {
showNumber?: boolean; showNumber?: boolean;
selectable?: boolean; selectable?: boolean;
minimumDefinitionCount?: number; minimumDefinitionCount?: number;
styleRules?: GridRowStyleRule[];
} }
export type GridRow = { export type GridRow = {
index: number; index: number;
ranged: boolean; ranged: boolean;
using: boolean; using: boolean;
additionalStyle?: AdditionalStyle; additionalStyles: AdditionalStyle[];
} }
export function createRow(index: number, using: boolean): GridRow { export function createRow(index: number, using: boolean): GridRow {
@ -24,12 +39,13 @@ export function createRow(index: number, using: boolean): GridRow {
index, index,
ranged: false, ranged: false,
using: using, using: using,
additionalStyles: [],
}; };
} }
export function resetRow(row: GridRow): void { export function resetRow(row: GridRow): void {
row.ranged = false; row.ranged = false;
row.using = false; row.using = false;
row.additionalStyle = undefined; row.additionalStyles = [];
} }

View File

@ -79,7 +79,7 @@
</MkFolder> </MkFolder>
<div :class="$style.gridArea"> <div :class="$style.gridArea">
<MkGrid :data="gridItems" :gridSetting="gridSetting" :columnSettings="columnSettings" @event="onGridEvent"/> <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
</div> </div>
<MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/> <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
@ -113,7 +113,6 @@ import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { GridColumnSetting } from '@/components/grid/column.js';
import { validators } from '@/components/grid/cell-validators.js'; import { validators } from '@/components/grid/cell-validators.js';
import { import {
GridCellContextMenuEvent, GridCellContextMenuEvent,
@ -131,7 +130,7 @@ import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';
import { GridRowSetting } from '@/components/grid/row.js'; import { GridSetting } from '@/components/grid/grid.js';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -146,26 +145,42 @@ type GridItem = {
localOnly: boolean; localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string; roleIdsThatCanBeUsedThisEmojiAsReaction: string;
fileId?: string; fileId?: string;
updatedAt: string | null;
} }
const gridSetting: GridRowSetting = { function setupGrid(): GridSetting {
showNumber: true, const required = validators.required();
selectable: false, const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
}; return {
row: {
const required = validators.required(); showNumber: true,
const regex = validators.regex(/^[a-zA-Z0-9_]+$/); selectable: true,
const columnSettings: GridColumnSetting[] = [ minimumDefinitionCount: 100,
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, styleRules: [
{ 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] }, condition: ({ row }) => JSON.stringify(gridItems.value[row.index]) !== JSON.stringify(originGridItems.value[row.index]),
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, applyStyle: { className: 'changedRow' },
{ 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 }, condition: ({ cells }) => cells.some(it => !it.violation.valid),
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, applyStyle: { className: 'violationRow' },
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140 }, },
]; ],
},
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<Misskey.entities.EmojiDetailedAdmin[]>([]); const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const allPages = ref<number>(0); const allPages = ref<number>(0);
@ -229,7 +244,7 @@ async function onUpdateButtonClicked() {
isSensitive: item.isSensitive, isSensitive: item.isSensitive,
localOnly: item.localOnly, localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction), roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
fileId: item.fileId, fileId: item.fileId,
}) })
.then(() => ({ item, success: true, err: undefined })) .then(() => ({ item, success: true, err: undefined }))
.catch(err => ({ item, success: false, err })), .catch(err => ({ item, success: false, err })),
@ -399,15 +414,6 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
} else { } else {
gridItems.value[row.index][column.setting.bindTo] = newValue; 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) { for (const cell of ranges) {
if (cell.column.setting.editable) { if (cell.column.setting.editable) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = undefined; 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, isSensitive: it.isSensitive,
localOnly: it.localOnly, localOnly: it.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '), roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '),
updatedAt: it.updatedAt,
})); }));
originGridItems.value = JSON.parse(JSON.stringify(gridItems.value)); originGridItems.value = JSON.parse(JSON.stringify(gridItems.value));
} }
@ -529,6 +528,16 @@ onMounted(async () => {
</script> </script>
<style lang="scss">
.violationRow {
background-color: var(--infoWarnBg);
}
.changedRow {
background-color: var(--infoBg);
}
</style>
<style lang="scss"> <style lang="scss">
.editedRow { .editedRow {
background-color: var(--infoBg); background-color: var(--infoBg);

View File

@ -7,9 +7,8 @@
<div> <div>
<div v-if="filteredLogs.length > 0"> <div v-if="filteredLogs.length > 0">
<MkGrid <MkGrid
:gridSetting="{ rowNumberVisible: false, rowSelectable: false }"
:data="filteredLogs" :data="filteredLogs"
:columnSettings="columnSettings" :settings="setupGrid()"
@event="onGridEvent" @event="onGridEvent"
/> />
</div> </div>
@ -39,13 +38,22 @@ import {
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import { GridSetting } from '@/components/grid/grid.js';
const columnSettings: GridColumnSetting[] = [ function setupGrid(): GridSetting {
{ bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 }, return {
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, row: {
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 }, showNumber: false,
{ bindTo: 'error', title: 'log', type: 'text', editable: false, width: 'auto' }, selectable: false,
]; },
cols: [
{ bindTo: 'failed', title: 'failed', type: 'boolean', editable: false, width: 50 },
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 140 },
{ bindTo: 'error', title: 'log', type: 'text', editable: false, width: 'auto' },
],
};
}
const props = defineProps<{ const props = defineProps<{
logs: RequestLogItem[]; logs: RequestLogItem[];

View File

@ -54,7 +54,7 @@
<div v-if="gridItems.length > 0" :class="$style.gridArea"> <div v-if="gridItems.length > 0" :class="$style.gridArea">
<MkGrid <MkGrid
:data="gridItems" :data="gridItems"
:columnSettings="columnSettings" :settings="setupGrid()"
@event="onGridEvent" @event="onGridEvent"
/> />
</div> </div>
@ -75,11 +75,7 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { import { emptyStrToEmptyArray, emptyStrToNull, RequestLogItem } from '@/pages/admin/custom-emojis-grid.impl.js';
emptyStrToEmptyArray,
emptyStrToNull,
RequestLogItem,
} from '@/pages/admin/custom-emojis-grid.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue'; import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
@ -100,10 +96,10 @@ import {
GridKeyDownEvent, GridKeyDownEvent,
GridRowContextMenuEvent, GridRowContextMenuEvent,
} from '@/components/grid/grid-event.js'; } from '@/components/grid/grid-event.js';
import { GridColumnSetting } from '@/components/grid/column.js';
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js'; import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
import { optInGridUtils } from '@/components/grid/optin-utils.js'; import { optInGridUtils } from '@/components/grid/optin-utils.js';
import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue'; import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue';
import { GridSetting } from '@/components/grid/grid.js';
const MAXIMUM_EMOJI_REGISTER_COUNT = 100; const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
@ -125,18 +121,34 @@ type GridItem = {
roleIdsThatCanBeUsedThisEmojiAsReaction: string; roleIdsThatCanBeUsedThisEmojiAsReaction: string;
} }
const required = validators.required(); function setupGrid(): GridSetting {
const regex = validators.regex(/^[a-zA-Z0-9_]+$/); const required = validators.required();
const columnSettings: GridColumnSetting[] = [ const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
{ 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] }, return {
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, row: {
{ bindTo: 'aliases', title: 'aliases', type: 'text', editable: true, width: 140 }, showNumber: true,
{ bindTo: 'license', title: 'license', type: 'text', editable: true, width: 140 }, selectable: true,
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 }, minimumDefinitionCount: 100,
{ bindTo: 'localOnly', title: 'localOnly', type: 'boolean', editable: true, width: 90 }, styleRules: [
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 100 }, {
]; condition: ({ cells }) => cells.some(it => !it.violation.valid),
applyStyle: { className: 'violationRow' },
},
],
},
cols: [
{ 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: 100 },
],
};
}
const uploadFolders = ref<FolderItem[]>([]); const uploadFolders = ref<FolderItem[]>([]);
const gridItems = ref<GridItem[]>([]); const gridItems = ref<GridItem[]>([]);
@ -284,7 +296,7 @@ async function onFileSelectClicked() {
} }
async function onDriveSelectClicked() { async function onDriveSelectClicked() {
const driveFiles = await os.promiseDialog(chooseFileFromDrive(true)); const driveFiles = await chooseFileFromDrive(true);
gridItems.value.push(...driveFiles.map(fromDriveFile)); gridItems.value.push(...driveFiles.map(fromDriveFile));
} }
@ -382,6 +394,12 @@ onMounted(async () => {
}); });
</script> </script>
<style lang="scss">
.violationRow {
background-color: var(--infoWarnBg);
}
</style>
<style module lang="scss"> <style module lang="scss">
.uploadBox { .uploadBox {
display: flex; display: flex;

View File

@ -26,7 +26,7 @@
<div v-if="gridItems.length > 0"> <div v-if="gridItems.length > 0">
<div :class="$style.gridArea"> <div :class="$style.gridArea">
<MkGrid :data="gridItems" :columnSettings="columnSettings" @event="onGridEvent"/> <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
</div> </div>
<div class="_gaps"> <div class="_gaps">
@ -65,6 +65,7 @@ import { optInGridUtils } from '@/components/grid/optin-utils.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue'; import XRegisterLogs from '@/pages/admin/custom-emojis-grid.local.logs.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { GridSetting } from '@/components/grid/grid.js';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -74,12 +75,16 @@ type GridItem = {
host: string; host: string;
} }
const columnSettings: GridColumnSetting[] = [ function setupGrid(): GridSetting {
{ bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 }, return {
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' }, cols: [
{ bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' }, { bindTo: 'checked', icon: 'ti-download', type: 'boolean', editable: true, width: 34 },
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' }, { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto' },
]; { bindTo: 'name', title: 'name', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'host', title: 'host', type: 'text', editable: false, width: 'auto' },
],
};
}
const requestLogs = ref<RequestLogItem[]>([]); const requestLogs = ref<RequestLogItem[]>([]);