fix validation and register roles

This commit is contained in:
samunohito 2024-02-26 08:12:37 +09:00
parent cb668b22ad
commit 390af67949
8 changed files with 99 additions and 57 deletions

View File

@ -269,7 +269,7 @@ useTooltip(rootEl, (showing) => {
return; return;
} }
const content = cell.value.violation.violations.map(it => it.result.message).join('\n'); const content = cell.value.violation.violations.filter(it => !it.valid).map(it => it.result.message).join('\n');
os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), { os.popup(defineAsyncComponent(() => import('@/components/grid/MkCellTooltip.vue')), {
showing, showing,
content, content,

View File

@ -76,12 +76,14 @@ const props = defineProps<{
}>(); }>();
// non-reactive // non-reactive
// eslint-disable-next-line vue/no-setup-props-destructure
const rowSetting: Required<GridRowSetting> = { const rowSetting: Required<GridRowSetting> = {
...defaultGridRowSetting, ...defaultGridRowSetting,
...props.settings.row, ...props.settings.row,
}; };
// non-reactive // non-reactive
// eslint-disable-next-line vue/no-setup-props-destructure
const columnSettings = props.settings.cols; const columnSettings = props.settings.cols;
// non-reactive // non-reactive
@ -1117,21 +1119,22 @@ function refreshData() {
// //
// //
const _cells: RowHolder[] = _rows.map(row => { const _cells: RowHolder[] = _rows.map(row => {
const cells = row.using const newCells = row.using
? _cols.map(col => { ? _cols.map(col => createCell(col, row, _data[row.index][col.setting.bindTo], cellSettings))
const cell = createCell(col, row, _data[row.index][col.setting.bindTo], cellSettings);
cell.violation = cellValidation(cell, cell.value);
return cell;
})
: _cols.map(col => createCell(col, row, undefined, cellSettings)); : _cols.map(col => createCell(col, row, undefined, cellSettings));
return { row, cells, origin: _data[row.index] }; return { row, cells: newCells, origin: _data[row.index] };
}); });
rows.value = _rows; rows.value = _rows;
cells.value = _cells; cells.value = _cells;
applyRowRules(_cells.filter(it => it.row.using).flatMap(it => it.cells)); const allCells = _cells.filter(it => it.row.using).flatMap(it => it.cells);
for (const cell of allCells) {
cell.violation = cellValidation(allCells, cell, cell.value);
}
applyRowRules(allCells);
if (_DEV_) { if (_DEV_) {
console.log('[grid][refresh-data][end]'); console.log('[grid][refresh-data][end]');
@ -1205,7 +1208,6 @@ function patchData(newItems: DataSource[]) {
const oldCell = oldCells[colIdx]; const oldCell = oldCells[colIdx];
const newValue = newItem[_col.setting.bindTo]; const newValue = newItem[_col.setting.bindTo];
if (oldCell.value !== newValue) { if (oldCell.value !== newValue) {
oldCell.violation = cellValidation(oldCell, newValue);
oldCell.value = _col.setting.valueTransformer oldCell.value = _col.setting.valueTransformer
? _col.setting.valueTransformer(holder.row, _col, newValue) ? _col.setting.valueTransformer(holder.row, _col, newValue)
: newValue; : newValue;
@ -1215,6 +1217,11 @@ function patchData(newItems: DataSource[]) {
} }
if (changedCells.length > 0) { if (changedCells.length > 0) {
const allCells = cells.value.slice(0, newItems.length).flatMap(it => it.cells);
for (const cell of allCells) {
cell.violation = cellValidation(allCells, cell, cell.value);
}
applyRowRules(changedCells); applyRowRules(changedCells);
// //

View File

@ -6,6 +6,7 @@ export type ValidatorParams = {
column: GridColumn; column: GridColumn;
row: GridRow; row: GridRow;
value: CellValue; value: CellValue;
allCells: GridCell[];
}; };
export type ValidatorResult = { export type ValidatorResult = {
@ -13,7 +14,7 @@ export type ValidatorResult = {
message?: string; message?: string;
} }
export type CellValidator = { export type GridCellValidator = {
name?: string; name?: string;
ignoreViolation?: boolean; ignoreViolation?: boolean;
validate: (params: ValidatorParams) => ValidatorResult; validate: (params: ValidatorParams) => ValidatorResult;
@ -27,11 +28,11 @@ export type ValidateViolation = {
export type ValidateViolationItem = { export type ValidateViolationItem = {
valid: boolean; valid: boolean;
validator: CellValidator; validator: GridCellValidator;
result: ValidatorResult; result: ValidatorResult;
} }
export function cellValidation(cell: GridCell, newValue: CellValue): ValidateViolation { export function cellValidation(allCells: GridCell[], cell: GridCell, newValue: CellValue): ValidateViolation {
const { column, row } = cell; const { column, row } = cell;
const validators = column.setting.validators ?? []; const validators = column.setting.validators ?? [];
@ -39,6 +40,7 @@ export function cellValidation(cell: GridCell, newValue: CellValue): ValidateVio
column, column,
row, row,
value: newValue, value: newValue,
allCells,
}; };
const violations: ValidateViolationItem[] = validators.map(validator => { const violations: ValidateViolationItem[] = validators.map(validator => {
@ -58,11 +60,10 @@ export function cellValidation(cell: GridCell, newValue: CellValue): ValidateVio
} }
class ValidatorPreset { class ValidatorPreset {
required(): CellValidator { required(): GridCellValidator {
return { return {
name: 'required', name: 'required',
validate: (params: ValidatorParams): ValidatorResult => { validate: ({ value }): ValidatorResult => {
const { value } = params;
return { return {
valid: value !== null && value !== undefined && value !== '', valid: value !== null && value !== undefined && value !== '',
message: 'This field is required.', message: 'This field is required.',
@ -71,11 +72,10 @@ class ValidatorPreset {
}; };
} }
regex(pattern: RegExp): CellValidator { regex(pattern: RegExp): GridCellValidator {
return { return {
name: 'regex', name: 'regex',
validate: (params: ValidatorParams): ValidatorResult => { validate: ({ value, column }): ValidatorResult => {
const { value, column } = params;
if (column.setting.type !== 'text') { if (column.setting.type !== 'text') {
return { return {
valid: false, valid: false,
@ -90,6 +90,22 @@ class ValidatorPreset {
}, },
}; };
} }
unique(): GridCellValidator {
return {
name: 'unique',
validate: ({ column, row, value, allCells }): ValidatorResult => {
const bindTo = column.setting.bindTo;
const isUnique = allCells
.filter(it => it.column.setting.bindTo === bindTo && it.row.index !== row.index)
.every(cell => cell.value !== value);
return {
valid: isUnique,
message: 'This value is already used.',
};
},
};
}
} }
export const validators = new ValidatorPreset(); export const validators = new ValidatorPreset();

View File

@ -1,4 +1,4 @@
import { CellValidator } from '@/components/grid/cell-validators.js'; import { GridCellValidator } from '@/components/grid/cell-validators.js';
import { 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 } from '@/components/grid/cell.js'; import { CellValue, GridCell } from '@/components/grid/cell.js';
@ -19,7 +19,7 @@ export type GridColumnSetting = {
type: ColumnType; type: ColumnType;
width: SizeStyle; width: SizeStyle;
editable?: boolean; editable?: boolean;
validators?: CellValidator[]; validators?: GridCellValidator[];
customValueEditor?: CustomValueEditor; customValueEditor?: CustomValueEditor;
valueTransformer?: CellValueTransformer; valueTransformer?: CellValueTransformer;
contextMenuFactory?: GridColumnContextMenuFactory; contextMenuFactory?: GridColumnContextMenuFactory;

View File

@ -17,3 +17,20 @@ export function emptyStrToEmptyArray(value: string) {
return value === '' ? [] : value.split(',').map(it => it.trim()); return value === '' ? [] : value.split(',').map(it => it.trim());
} }
export function roleIdsParser(text: string): { id: string, name: string }[] {
// idとnameのペア配列をJSONで受け取る。それ以外の形式は許容しない
try {
const obj = JSON.parse(text);
if (!Array.isArray(obj)) {
return [];
}
if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
return [];
}
return obj.map(it => ({ id: it.id, name: it.name }));
} catch (ex) {
console.warn(ex);
return [];
}
}

View File

@ -189,13 +189,13 @@ import {
emptyStrToEmptyArray, emptyStrToEmptyArray,
emptyStrToNull, emptyStrToNull,
emptyStrToUndefined, emptyStrToUndefined,
RequestLogItem, RequestLogItem, roleIdsParser,
} from '@/pages/admin/custom-emojis-manager.impl.js'; } from '@/pages/admin/custom-emojis-manager.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 MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { validators } from '@/components/grid/cell-validators.js'; import { GridCellValidator, validators } from '@/components/grid/cell-validators.js';
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue'; import MkPagingButtons from '@/components/MkPagingButtons.vue';
@ -247,6 +247,7 @@ type GridSortOrder = {
function setupGrid(): GridSetting { function setupGrid(): GridSetting {
const required = validators.required(); const required = validators.required();
const regex = validators.regex(/^[a-zA-Z0-9_]+$/); const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
const unique = validators.unique();
return { return {
row: { row: {
showNumber: true, showNumber: true,
@ -302,7 +303,10 @@ function setupGrid(): GridSetting {
return file.url; return file.url;
}, },
}, },
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required, regex] }, {
bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140,
validators: [required, regex, unique],
},
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
{ bindTo: 'aliases', title: 'aliases', 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: 'license', title: 'license', type: 'text', editable: true, width: 140 },
@ -335,23 +339,7 @@ function setupGrid(): GridSetting {
return transform; return transform;
}, },
events: { events: {
paste(text) { paste: roleIdsParser,
// idnameJSON
try {
const obj = JSON.parse(text);
if (!Array.isArray(obj)) {
return [];
}
if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
return [];
}
return obj.map(it => ({ id: it.id, name: it.name }));
} catch (ex) {
console.warn(ex);
return [];
}
},
delete(cell) { delete(cell) {
// undefined // undefined
gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = []; gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = [];

View File

@ -75,7 +75,12 @@
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 { emptyStrToEmptyArray, emptyStrToNull, RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; import {
emptyStrToEmptyArray,
emptyStrToNull,
RequestLogItem,
roleIdsParser,
} from '@/pages/admin/custom-emojis-manager.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';
@ -117,6 +122,7 @@ type GridItem = {
function setupGrid(): GridSetting { function setupGrid(): GridSetting {
const required = validators.required(); const required = validators.required();
const regex = validators.regex(/^[a-zA-Z0-9_]+$/); const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
const unique = validators.unique();
function removeRows(rows: GridRow[]) { function removeRows(rows: GridRow[]) {
const idxes = [...new Set(rows.map(it => it.index))]; const idxes = [...new Set(rows.map(it => it.index))];
@ -158,7 +164,10 @@ function setupGrid(): GridSetting {
}, },
cols: [ cols: [
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] }, { 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: 'name', title: 'name', type: 'text', editable: true, width: 140,
validators: [required, regex, unique],
},
{ bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 }, { bindTo: 'category', title: 'category', type: 'text', editable: true, width: 140 },
{ bindTo: 'aliases', title: 'aliases', 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: 'license', title: 'license', type: 'text', editable: true, width: 140 },
@ -190,6 +199,13 @@ function setupGrid(): GridSetting {
return transform; return transform;
}, },
events: {
paste: roleIdsParser,
delete(cell) {
// undefined
gridItems.value[cell.row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = [];
},
},
}, },
], ],
cells: { cells: {
@ -343,8 +359,7 @@ async function onDrop(ev: DragEvent) {
} }
async function onFileSelectClicked() { async function onFileSelectClicked() {
const driveFiles = await os.promiseDialog( const driveFiles = await chooseFileFromPc(
chooseFileFromPc(
true, true,
{ {
uploadFolder: selectedFolderId.value, uploadFolder: selectedFolderId.value,
@ -352,7 +367,6 @@ async function onFileSelectClicked() {
// //
nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''), nameConverter: (file) => file.name.replace(/\.[a-zA-Z0-9]+$/, ''),
}, },
),
); );
gridItems.value.push(...driveFiles.map(fromDriveFile)); gridItems.value.push(...driveFiles.map(fromDriveFile));

View File

@ -118,18 +118,18 @@ function createRender(params: {
const body = await new Response(bodyStream).json() as entities.AdminEmojiAddRequest; const body = await new Response(bodyStream).json() as entities.AdminEmojiAddRequest;
const fileId = body.fileId; const fileId = body.fileId;
const file = storedDriveFiles.find(f => f.id === fileId); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const file = storedDriveFiles.find(f => f.id === fileId)!;
const em = emoji({ const em = emoji({
id: fakeId(), id: fakeId(),
name: body.name, name: body.name,
url: body.url,
publicUrl: file.url, publicUrl: file.url,
originalUrl: file.url, originalUrl: file.url,
type: file.type, type: file.type,
aliases: body.aliases, aliases: body.aliases,
category: body.category, category: body.category ?? undefined,
license: body.license, license: body.license ?? undefined,
localOnly: body.localOnly, localOnly: body.localOnly,
isSensitive: body.isSensitive, isSensitive: body.isSensitive,
}); });