support role select

This commit is contained in:
samunohito 2024-02-17 19:51:44 +09:00
parent c0f941689b
commit 07b9757b36
11 changed files with 433 additions and 81 deletions

View File

@ -444,12 +444,12 @@ export class CustomEmojiService implements OnApplicationShutdown {
function multipleWordsToQuery( function multipleWordsToQuery(
query: string, query: string,
builder: SelectQueryBuilder<MiEmoji>, builder: SelectQueryBuilder<MiEmoji>,
action: (qb: WhereExpressionBuilder, word: string) => void, action: (qb: WhereExpressionBuilder, idx: number, word: string) => void,
) { ) {
const words = query.split(/\s/); const words = query.split(/\s/);
builder.andWhere(new Brackets((qb => { builder.andWhere(new Brackets((qb => {
for (const word of words) { for (const [idx, word] of words.entries()) {
action(qb, word); action(qb, idx, word);
} }
}))); })));
} }
@ -466,8 +466,8 @@ export class CustomEmojiService implements OnApplicationShutdown {
builder.andWhere('emoji.updatedAt <= :updateAtTo', { updateAtTo: q.updatedAtTo }); builder.andWhere('emoji.updatedAt <= :updateAtTo', { updateAtTo: q.updatedAtTo });
} }
if (q.name) { if (q.name) {
multipleWordsToQuery(q.name, builder, (qb, word) => { multipleWordsToQuery(q.name, builder, (qb, idx, word) => {
qb.orWhere('emoji.name LIKE :name', { name: `%${word}%` }); qb.orWhere(`emoji.name LIKE :name${idx}`, Object.fromEntries([[`name${idx}`, `%${word}%`]]));
}); });
} }
@ -479,8 +479,8 @@ export class CustomEmojiService implements OnApplicationShutdown {
case q.hostType === 'remote': { case q.hostType === 'remote': {
if (q.host) { if (q.host) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.host, builder, (qb, word) => { multipleWordsToQuery(q.host, builder, (qb, idx, word) => {
qb.orWhere('emoji.host LIKE :host', { host: `%${word}%` }); qb.orWhere(`emoji.host LIKE :host${idx}`, Object.fromEntries([[`host${idx}`, `%${word}%`]]));
}); });
} else { } else {
builder.andWhere('emoji.host IS NOT NULL'); builder.andWhere('emoji.host IS NOT NULL');
@ -491,38 +491,38 @@ export class CustomEmojiService implements OnApplicationShutdown {
if (q.uri) { if (q.uri) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.uri, builder, (qb, word) => { multipleWordsToQuery(q.uri, builder, (qb, idx, word) => {
qb.orWhere('emoji.uri LIKE :uri', { uri: `%${word}%` }); qb.orWhere(`emoji.uri LIKE :uri${idx}`, Object.fromEntries([[`uri${idx}`, `%${word}%`]]));
}); });
} }
if (q.publicUrl) { if (q.publicUrl) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.publicUrl, builder, (qb, word) => { multipleWordsToQuery(q.publicUrl, builder, (qb, idx, word) => {
qb.orWhere('emoji.publicUrl LIKE :publicUrl', { publicUrl: `%${word}%` }); qb.orWhere(`emoji.publicUrl LIKE :publicUrl${idx}`, Object.fromEntries([[`publicUrl${idx}`, `%${word}%`]]));
}); });
} }
if (q.type) { if (q.type) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.type, builder, (qb, word) => { multipleWordsToQuery(q.type, builder, (qb, idx, word) => {
qb.orWhere('emoji.type LIKE :type', { type: `%${word}%` }); qb.orWhere(`emoji.type LIKE :type${idx}`, Object.fromEntries([[`type${idx}`, `%${word}%`]]));
}); });
} }
if (q.aliases) { if (q.aliases) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.aliases, builder, (qb, word) => { multipleWordsToQuery(q.aliases, builder, (qb, idx, word) => {
qb.orWhere('emoji.aliases LIKE :aliases', { aliases: `%${word}%` }); qb.orWhere(`emoji.aliases LIKE :aliases${idx}`, Object.fromEntries([[`aliases${idx}`, `%${word}%`]]));
}); });
} }
if (q.category) { if (q.category) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.category, builder, (qb, word) => { multipleWordsToQuery(q.category, builder, (qb, idx, word) => {
qb.orWhere('emoji.category LIKE :category', { category: `%${word}%` }); qb.orWhere(`emoji.category LIKE :category${idx}`, Object.fromEntries([[`category${idx}`, `%${word}%`]]));
}); });
} }
if (q.license) { if (q.license) {
// noIndexScan // noIndexScan
multipleWordsToQuery(q.license, builder, (qb, word) => { multipleWordsToQuery(q.license, builder, (qb, idx, word) => {
qb.orWhere('emoji.license LIKE :license', { license: `%${word}%` }); qb.orWhere(`emoji.license LIKE :license${idx}`, Object.fromEntries([[`license${idx}`, `%${word}%`]]));
}); });
} }
if (q.isSensitive != null) { if (q.isSensitive != null) {
@ -533,6 +533,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
// noIndexScan // noIndexScan
builder.andWhere('emoji.localOnly = :localOnly', { localOnly: q.localOnly }); builder.andWhere('emoji.localOnly = :localOnly', { localOnly: q.localOnly });
} }
if (q.roleIds && q.roleIds.length > 0) {
builder.andWhere('emoji.roleIdsThatCanBeUsedThisEmojiAsReaction @> :roleIds', { roleIds: q.roleIds });
}
} }
if (params?.sinceId) { if (params?.sinceId) {
@ -542,7 +545,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
builder.andWhere('emoji.id < :untilId', { untilId: params.untilId }); builder.andWhere('emoji.id < :untilId', { untilId: params.untilId });
} }
if (params?.sort) { if (params?.sort && params.sort.length > 0) {
for (const sort of params.sort) { for (const sort of params.sort) {
builder.addOrderBy(`emoji.${sort.key}`, sort.direction); builder.addOrderBy(`emoji.${sort.key}`, sort.direction);
} }

View File

@ -97,6 +97,14 @@ export class EmojiEntityService {
...await this.rolesRepository.findBy({ id: In(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) }), ...await this.rolesRepository.findBy({ id: In(emoji.roleIdsThatCanBeUsedThisEmojiAsReaction) }),
); );
} }
roles.sort((a, b) => {
if (a.displayOrder !== b.displayOrder) {
return b.displayOrder - a.displayOrder;
}
return a.id.localeCompare(b.id);
});
} }
return { return {

View File

@ -0,0 +1,177 @@
<template>
<MkWindow
ref="windowEl"
:initialWidth="400"
:initialHeight="500"
:canResize="false"
@close="windowEl?.close()"
@closed="$emit('closed')"
>
<template #header>{{ title }}</template>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps">
<div :class="$style.header">
<MkButton rounded @click="addRole"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
</div>
<div>
<div v-if="selectedRoles.length > 0" class="_gaps" :class="$style.roleItemArea">
<div v-for="role in selectedRoles" :key="role.id" :class="$style.roleItem">
<MkRolePreview :class="$style.role" :role="role" :forModeration="true" :detailed="false" style="pointer-events: none;"/>
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnAssign" @click="removeRole(role.id)"><i class="ti ti-x"></i></button>
<button v-else class="_button" :class="$style.roleUnAssign" disabled><i class="ti ti-ban"></i></button>
</div>
</div>
<div v-else :class="$style.roleItemArea" style="text-align: center">
何も選択されていません
</div>
</div>
<MkInfo v-if="infoMessage">{{ infoMessage }}</MkInfo>
<div :class="$style.buttons">
<MkButton primary @click="onOkClicked">{{ i18n.ts.ok }}</MkButton>
<MkButton @click="onCancelClicked">{{ i18n.ts.cancel }}</MkButton>
</div>
</div>
</MkSpacer>
</MkWindow>
</template>
<script setup lang="ts">
import { computed, defineProps, ref, toRefs } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkRolePreview from '@/components/MkRolePreview.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import Section from '@/components/form/section.vue';
import * as os from '@/os.js';
import MkWindow from '@/components/MkWindow.vue';
import MkSpacer from '@/components/global/MkSpacer.vue';
const emit = defineEmits<{
(ev: 'done', value: Misskey.entities.Role[]),
(ev: 'closed'),
}>();
const props = withDefaults(defineProps<{
initialRoleIds?: string[],
infoMessage?: string,
title?: string,
publicOnly: boolean,
}>(), {
initialRoleIds: undefined,
infoMessage: undefined,
title: undefined,
publicOnly: true,
});
const { initialRoleIds, infoMessage, title, publicOnly } = toRefs(props);
const windowEl = ref<InstanceType<typeof MkWindow>>();
const roles = ref<Misskey.entities.Role[]>([]);
const selectedRoleIds = ref<string[]>(initialRoleIds.value ?? []);
const selectedRoles = computed(() => {
const r = roles.value.filter(role => selectedRoleIds.value.includes(role.id));
r.sort((a, b) => {
if (a.displayOrder !== b.displayOrder) {
return b.displayOrder - a.displayOrder;
}
return a.id.localeCompare(b.id);
});
return r;
});
async function fetchRoles() {
const result = await misskeyApi('admin/roles/list', {});
roles.value = result.filter(it => publicOnly.value ? it.isPublic : true);
}
async function addRole() {
const items = roles.value
.filter(r => r.isPublic)
.filter(r => !selectedRoleIds.value.includes(r.id))
.map(r => ({ text: r.name, value: r }));
const { canceled, result: role } = await os.select({ items });
if (canceled) {
return;
}
selectedRoleIds.value.push(role.id);
}
async function removeRole(roleId: string) {
selectedRoleIds.value = selectedRoleIds.value.filter(x => x !== roleId);
}
function onOkClicked() {
emit('done', selectedRoles.value);
windowEl.value?.close();
}
function onCancelClicked() {
emit('closed');
}
fetchRoles();
</script>
<style module lang="scss">
.roleItemArea {
background-color: var(--acrylicBg);
border-radius: var(--radius);
padding: 12px;
}
.roleItem {
display: flex;
}
.role {
flex: 1;
}
.roleUnAssign {
width: 32px;
height: 32px;
margin-left: 8px;
align-self: center;
}
.header {
display: flex;
align-items: center;
justify-content: flex-start;
}
.title {
flex: 1;
}
.addRoleButton {
min-width: 32px;
min-height: 32px;
max-width: 32px;
max-height: 32px;
margin-left: 8px;
align-self: center;
padding: 0;
}
.buttons {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.divider {
border-top: solid 0.5px var(--divider);
}
</style>

View File

@ -59,7 +59,6 @@ import * as os from '@/os.js';
import { CellValue, GridCell } from '@/components/grid/cell.js'; import { CellValue, GridCell } from '@/components/grid/cell.js';
import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js'; import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
import { GridRowSetting } from '@/components/grid/row.js'; import { GridRowSetting } from '@/components/grid/row.js';
import { selectFile } from '@/scripts/select-file.js';
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void; (ev: 'operation:beginEdit', sender: GridCell): void;
@ -107,7 +106,7 @@ watch(() => cell.value.selected, () => {
function onCellDoubleClick(ev: MouseEvent) { function onCellDoubleClick(ev: MouseEvent) {
switch (ev.type) { switch (ev.type) {
case 'dblclick': { case 'dblclick': {
beginEditing(); beginEditing(ev.target as HTMLElement);
break; break;
} }
} }
@ -127,7 +126,7 @@ function onCellKeyDown(ev: KeyboardEvent) {
case 'NumpadEnter': case 'NumpadEnter':
case 'Enter': case 'Enter':
case 'F2': { case 'F2': {
beginEditing(); beginEditing(ev.target as HTMLElement);
break; break;
} }
} }
@ -164,11 +163,27 @@ function unregisterOutsideMouseDown() {
removeEventListener('mousedown', onOutsideMouseDown); removeEventListener('mousedown', onOutsideMouseDown);
} }
async function beginEditing() { async function beginEditing(target: HTMLElement) {
if (editing.value || !cell.value.column.setting.editable) { if (editing.value || !cell.value.column.setting.editable) {
return; return;
} }
if (cell.value.column.setting.customValueEditor) {
emit('operation:beginEdit', cell.value);
const newValue = await cell.value.column.setting.customValueEditor(
cell.value.row,
cell.value.column,
cell.value.value,
target,
);
emit('operation:endEdit', cell.value);
if (newValue !== cell.value.value) {
emitValueChange(newValue);
}
rootEl.value?.focus();
} else {
switch (cellType.value) { switch (cellType.value) {
case 'text': { case 'text': {
editingValue.value = cell.value.value; editingValue.value = cell.value.value;
@ -189,12 +204,6 @@ async function beginEditing() {
emitValueChange(!cell.value.value); emitValueChange(!cell.value.value);
break; break;
} }
case 'image': {
const file = await selectFile(rootEl.value);
if (file) {
emitValueChange(JSON.stringify(file));
}
break;
} }
} }
} }

View File

@ -1170,7 +1170,9 @@ function patchData(newItems: DataSource[]) {
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.violation = cellValidation(oldCell, newValue);
oldCell.value = newValue; oldCell.value = _col.setting.valueTransformer
? _col.setting.valueTransformer(holder.row, _col, newValue)
: newValue;
changedCells.push(oldCell); changedCells.push(oldCell);
} }
} }
@ -1199,6 +1201,8 @@ function patchData(newItems: DataSource[]) {
// #endregion // #endregion
onMounted(() => { onMounted(() => {
state.value = 'normal';
const bindToList = columnSettings.map(it => it.bindTo); const bindToList = columnSettings.map(it => it.bindTo);
if (new Set(bindToList).size !== columnSettings.length) { if (new Set(bindToList).size !== columnSettings.length) {
// //

View File

@ -5,7 +5,7 @@ import { GridRow } from '@/components/grid/row.js';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { GridContext } from '@/components/grid/grid-event.js'; import { GridContext } from '@/components/grid/grid-event.js';
export type CellValue = string | boolean | number | undefined | null export type CellValue = string | boolean | number | undefined | null | Array<unknown> | Object;
export type CellAddress = { export type CellAddress = {
row: number; row: number;
@ -41,9 +41,13 @@ export function createCell(
value: CellValue, value: CellValue,
setting: GridCellSetting, setting: GridCellSetting,
): GridCell { ): GridCell {
const newValue = (row.using && column.setting.valueTransformer)
? column.setting.valueTransformer(row, column, value)
: value;
return { return {
address: { row: row.index, col: column.index }, address: { row: row.index, col: column.index },
value, value: newValue,
column, column,
row, row,
selected: false, selected: false,

View File

@ -8,7 +8,8 @@ import { GridContext } from '@/components/grid/grid-event.js';
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden'; export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image' | 'hidden';
export type CellValueConverter = (row: GridRow, col: GridColumn, value: CellValue) => CellValue; export type CustomValueEditor = (row: GridRow, col: GridColumn, value: CellValue, cellElement: HTMLElement) => Promise<CellValue>;
export type CellValueTransformer = (row: GridRow, col: GridColumn, value: CellValue) => CellValue;
export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[]; export type GridColumnContextMenuFactory = (col: GridColumn, context: GridContext) => MenuItem[];
export type GridColumnSetting = { export type GridColumnSetting = {
@ -19,7 +20,8 @@ export type GridColumnSetting = {
width: SizeStyle; width: SizeStyle;
editable?: boolean; editable?: boolean;
validators?: CellValidator[]; validators?: CellValidator[];
valueConverter?: CellValueConverter; customValueEditor?: CustomValueEditor;
valueTransformer?: CellValueTransformer;
contextMenuFactory?: GridColumnContextMenuFactory; contextMenuFactory?: GridColumnContextMenuFactory;
}; };

View File

@ -41,18 +41,22 @@ class OptInGridUtils {
} }
} }
copyToClipboard(gridItems: Ref<DataSource[]>, context: GridContext) { copyToClipboard(gridItems: Ref<DataSource[]> | DataSource[], context: GridContext) {
const items = typeof gridItems === 'object' ? (gridItems as Ref<DataSource[]>).value : gridItems;
const lines = Array.of<string>(); const lines = Array.of<string>();
const bounds = context.randedBounds; const bounds = context.randedBounds;
for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) { for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
const items = Array.of<string>(); const rowItems = Array.of<string>();
for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) { for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
const bindTo = context.columns[col].setting.bindTo; const bindTo = context.columns[col].setting.bindTo;
const cell = gridItems.value[row][bindTo]; const cell = items[row][bindTo];
items.push(cell?.toString() ?? ''); const value = typeof cell === 'object' || Array.isArray(cell)
? JSON.stringify(cell)
: cell?.toString() ?? '';
rowItems.push(value);
} }
lines.push(items.join('\t')); lines.push(rowItems.join('\t'));
} }
const text = lines.join('\n'); const text = lines.join('\n');
@ -66,9 +70,12 @@ class OptInGridUtils {
async pasteFromClipboard( async pasteFromClipboard(
gridItems: Ref<DataSource[]>, gridItems: Ref<DataSource[]>,
context: GridContext, context: GridContext,
valueConverters?: { bindTo: string, converter: (value: string) => CellValue }[],
) { ) {
function parseValue(value: string, type: GridColumnSetting['type']): CellValue { const converterMap = new Map<string, (value: string) => CellValue>(valueConverters?.map(it => [it.bindTo, it.converter]) ?? []);
switch (type) {
function parseValue(value: string, setting: GridColumnSetting): CellValue {
switch (setting.type) {
case 'number': { case 'number': {
return Number(value); return Number(value);
} }
@ -76,7 +83,9 @@ class OptInGridUtils {
return value === 'true'; return value === 'true';
} }
default: { default: {
return value; return converterMap.has(setting.bindTo)
? converterMap.get(setting.bindTo)!(value)
: value;
} }
} }
} }
@ -95,7 +104,7 @@ class OptInGridUtils {
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける // 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
const ranges = context.rangedCells; const ranges = context.rangedCells;
for (const cell of ranges) { for (const cell of ranges) {
gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting.type); gridItems.value[cell.row.index][cell.column.setting.bindTo] = parseValue(lines[0][0], cell.column.setting);
} }
} else { } else {
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける // 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
@ -117,13 +126,13 @@ class OptInGridUtils {
break; break;
} }
gridItems.value[row][columns[col].setting.bindTo] = parseValue(items[colIdx], columns[col].setting.type); gridItems.value[row][columns[col].setting.bindTo] = parseValue(items[colIdx], columns[col].setting);
} }
} }
} }
} }
deleteSelectionRange(gridItems: Ref<DataSource[]>, context: GridContext) { deleteSelectionRange(gridItems: Ref<Record<string, any>[]>, context: GridContext) {
if (context.rangedRows.length > 0) { if (context.rangedRows.length > 0) {
const deletedIndexes = context.rangedRows.map(it => it.index); const deletedIndexes = context.rangedRows.map(it => it.index);
gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index)); gridItems.value = gridItems.value.filter((_, index) => !deletedIndexes.includes(index));

View File

@ -475,6 +475,27 @@ export async function selectDriveFolder(multiple: boolean) {
}); });
} }
export async function selectRole(params: {
initialRoleIds?: string[],
title?: string,
infoMessage?: string,
publicOnly?: boolean,
}): Promise<
{ canceled: true; result: undefined; } |
{ canceled: false; result: Misskey.entities.Role[] }
> {
return new Promise((resolve) => {
popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
done: roles => {
resolve({ canceled: false, result: roles });
},
closed: () => {
resolve({ canceled: true, result: undefined });
},
}, 'closed');
});
}
export async function pickEmoji(src: HTMLElement | null, opts) { export async function pickEmoji(src: HTMLElement | null, opts) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
popup(MkEmojiPickerDialog, { popup(MkEmojiPickerDialog, {

View File

@ -56,6 +56,18 @@
<MkInput v-model="queryUpdatedAtTo" :debounce="true" type="date" autocapitalize="off" class="col3 row3"> <MkInput v-model="queryUpdatedAtTo" :debounce="true" type="date" autocapitalize="off" class="col3 row3">
<template #label>updatedAt(to)</template> <template #label>updatedAt(to)</template>
</MkInput> </MkInput>
<MkInput
v-model="queryRolesText"
:debounce="true"
type="text"
readonly
autocapitalize="off"
class="col1 row4"
@click="onQueryRolesEditClicked"
>
<template #label>role</template>
<template #suffix><span class="ti ti-pencil"/></template>
</MkInput>
</div> </div>
<MkFolder :spacerMax="8" :spacerMin="8"> <MkFolder :spacerMax="8" :spacerMin="8">
@ -153,6 +165,8 @@ import { deviceKind } from '@/scripts/device-kind.js';
import { GridSetting } from '@/components/grid/grid.js'; import { GridSetting } from '@/components/grid/grid.js';
import MkTagItem from '@/components/MkTagItem.vue'; import MkTagItem from '@/components/MkTagItem.vue';
import { MenuItem } from '@/types/menu.js'; import { MenuItem } from '@/types/menu.js';
import { CellValue } from '@/components/grid/cell.js';
import { selectFile } from '@/scripts/select-file.js';
type GridItem = { type GridItem = {
checked: boolean; checked: boolean;
@ -165,7 +179,7 @@ type GridItem = {
license: string; license: string;
isSensitive: boolean; isSensitive: boolean;
localOnly: boolean; localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string; roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
fileId?: string; fileId?: string;
updatedAt: string | null; updatedAt: string | null;
publicUrl?: string | null; publicUrl?: string | null;
@ -230,14 +244,54 @@ function setupGrid(): GridSetting {
}, },
cols: [ cols: [
{ bindTo: 'checked', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 }, { 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: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required],
customValueEditor: async (row, col, value, cellElement) => {
const file = await selectFile(cellElement);
if (file) {
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;
} else {
gridItems.value[row.index].url = '';
gridItems.value[row.index].fileId = undefined;
}
return file ? 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] },
{ 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 },
{ bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 }, { bindTo: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'localOnly', title: 'localOnly', 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: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140,
valueTransformer: (row) => {
// IDID
return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction
.map(({ name }) => name)
.join(',');
},
customValueEditor: async (row) => {
// ID使
const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id);
const result = await os.selectRole({
initialRoleIds: current,
title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction,
infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription,
publicOnly: true,
});
if (result.canceled) {
return current;
}
const transform = result.result.map(it => ({ id: it.id, name: it.name }));
gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform;
return transform;
},
},
{ bindTo: 'updatedAt', type: 'text', editable: false, width: 'auto' }, { bindTo: 'updatedAt', type: 'text', editable: false, width: 'auto' },
{ bindTo: 'publicUrl', type: 'text', editable: false, width: 180 }, { bindTo: 'publicUrl', type: 'text', editable: false, width: 180 },
{ bindTo: 'originalUrl', type: 'text', editable: false, width: 180 }, { bindTo: 'originalUrl', type: 'text', editable: false, width: 180 },
@ -249,7 +303,9 @@ function setupGrid(): GridSetting {
type: 'button', type: 'button',
text: '選択範囲をコピー', text: '選択範囲をコピー',
icon: 'ti ti-copy', icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context), action: () => {
return optInGridUtils.copyToClipboard(gridItems, context);
},
}, },
{ {
type: 'button', type: 'button',
@ -286,6 +342,7 @@ const queryUpdatedAtFrom = ref<string | null>(null);
const queryUpdatedAtTo = ref<string | null>(null); const queryUpdatedAtTo = ref<string | null>(null);
const querySensitive = ref<string | null>(null); const querySensitive = ref<string | null>(null);
const queryLocalOnly = ref<string | null>(null); const queryLocalOnly = ref<string | null>(null);
const queryRoles = ref<{ id: string, name: string }[]>([]);
const previousQuery = ref<string | undefined>(undefined); const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<GridSortOrder[]>([]); const sortOrders = ref<GridSortOrder[]>([]);
const requestLogs = ref<RequestLogItem[]>([]); const requestLogs = ref<RequestLogItem[]>([]);
@ -295,6 +352,7 @@ const originGridItems = ref<GridItem[]>([]);
const updateButtonDisabled = ref<boolean>(false); const updateButtonDisabled = ref<boolean>(false);
const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind)); const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
const queryRolesText = computed(() => queryRoles.value.map(it => it.name).join(','));
async function onUpdateButtonClicked() { async function onUpdateButtonClicked() {
const _items = gridItems.value; const _items = gridItems.value;
@ -334,7 +392,7 @@ async function onUpdateButtonClicked() {
license: emptyStrToNull(item.license), license: emptyStrToNull(item.license),
isSensitive: item.isSensitive, isSensitive: item.isSensitive,
localOnly: item.localOnly, localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction), roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
fileId: item.fileId, fileId: item.fileId,
}) })
.then(() => ({ item, success: true, err: undefined })) .then(() => ({ item, success: true, err: undefined }))
@ -402,6 +460,19 @@ function onGridResetButtonClicked() {
refreshGridItems(); refreshGridItems();
} }
async function onQueryRolesEditClicked() {
const result = await os.selectRole({
initialRoleIds: queryRoles.value.map(it => it.id),
title: '絵文字に設定されたロールで検索',
publicOnly: true,
});
if (result.canceled) {
return;
}
queryRoles.value = result.result;
}
function onToggleSortOrderButtonClicked(order: GridSortOrder) { function onToggleSortOrderButtonClicked(order: GridSortOrder) {
console.log(order); console.log(order);
switch (order.direction) { switch (order.direction) {
@ -446,6 +517,7 @@ function onQueryResetButtonClicked() {
queryUpdatedAtTo.value = null; queryUpdatedAtTo.value = null;
querySensitive.value = null; querySensitive.value = null;
queryLocalOnly.value = null; queryLocalOnly.value = null;
queryRoles.value = [];
} }
async function onPageChanged(pageNumber: number) { async function onPageChanged(pageNumber: number) {
@ -474,17 +546,27 @@ function onGridCellValidation(event: GridCellValidationEvent) {
function onGridCellValueChange(event: GridCellValueChangeEvent) { function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event; const { row, column, newValue } = event;
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) { if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
if (column.setting.bindTo === 'url') {
const file = JSON.parse(newValue as string) as Misskey.entities.DriveFile;
gridItems.value[row.index].url = file.url;
gridItems.value[row.index].fileId = file.id;
} else {
gridItems.value[row.index][column.setting.bindTo] = newValue; gridItems.value[row.index][column.setting.bindTo] = newValue;
} }
}
} }
async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) { async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext) {
function roleIdConverter(value: string): CellValue {
try {
const obj = JSON.parse(value);
if (!Array.isArray(obj)) {
return undefined;
}
if (!obj.every(it => typeof it === 'object' && 'id' in it && 'name' in it)) {
return undefined;
}
return obj.map(it => ({ id: it.id, name: it.name }));
} catch (ex) {
return undefined;
}
}
const { ctrlKey, shiftKey, code } = event.event; const { ctrlKey, shiftKey, code } = event.event;
switch (true) { switch (true) {
@ -498,7 +580,13 @@ async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext)
break; break;
} }
case 'KeyV': { case 'KeyV': {
await optInGridUtils.pasteFromClipboard(gridItems, currentState); await optInGridUtils.pasteFromClipboard(
gridItems,
currentState,
[
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', converter: roleIdConverter },
],
);
break; break;
} }
} }
@ -543,6 +631,7 @@ async function refreshCustomEmojis() {
localOnly: queryLocalOnly.value ? Boolean(queryLocalOnly.value).valueOf() : undefined, localOnly: queryLocalOnly.value ? Boolean(queryLocalOnly.value).valueOf() : undefined,
updatedAtFrom: emptyStrToUndefined(queryUpdatedAtFrom.value), updatedAtFrom: emptyStrToUndefined(queryUpdatedAtFrom.value),
updatedAtTo: emptyStrToUndefined(queryUpdatedAtTo.value), updatedAtTo: emptyStrToUndefined(queryUpdatedAtTo.value),
roleIds: queryRoles.value.map(it => it.id),
hostType: 'local', hostType: 'local',
}; };
@ -584,7 +673,7 @@ function refreshGridItems() {
license: it.license ?? '', license: it.license ?? '',
isSensitive: it.isSensitive, isSensitive: it.isSensitive,
localOnly: it.localOnly, localOnly: it.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(','), roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction,
updatedAt: it.updatedAt, updatedAt: it.updatedAt,
publicUrl: it.publicUrl, publicUrl: it.publicUrl,
originalUrl: it.originalUrl, originalUrl: it.originalUrl,

View File

@ -116,7 +116,7 @@ type GridItem = {
license: string; license: string;
isSensitive: boolean; isSensitive: boolean;
localOnly: boolean; localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string; roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
} }
function setupGrid(): GridSetting { function setupGrid(): GridSetting {
@ -159,7 +159,33 @@ function setupGrid(): GridSetting {
{ bindTo: 'license', title: 'license', 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: 'isSensitive', title: 'sensitive', type: 'boolean', editable: true, width: 90 },
{ bindTo: 'localOnly', title: 'localOnly', 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 }, {
bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', editable: true, width: 140,
valueTransformer: (row) => {
// IDID
return gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction
.map(({ name }) => name)
.join(',');
},
customValueEditor: async (row) => {
// ID使
const current = gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id);
const result = await os.selectRole({
initialRoleIds: current,
title: i18n.ts.rolesThatCanBeUsedThisEmojiAsReaction,
infoMessage: i18n.ts.rolesThatCanBeUsedThisEmojiAsReactionEmptyDescription,
publicOnly: true,
});
if (result.canceled) {
return current;
}
const transform = result.result.map(it => ({ id: it.id, name: it.name }));
gridItems.value[row.index].roleIdsThatCanBeUsedThisEmojiAsReaction = transform;
return transform;
},
},
], ],
cells: { cells: {
contextMenuFactory: (col, row, value, context) => { contextMenuFactory: (col, row, value, context) => {
@ -214,7 +240,7 @@ async function onRegistryClicked() {
license: emptyStrToNull(item.license), license: emptyStrToNull(item.license),
isSensitive: item.isSensitive, isSensitive: item.isSensitive,
localOnly: item.localOnly, localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction), roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
fileId: item.fileId!, fileId: item.fileId!,
}) })
.then(() => ({ item, success: true, err: undefined })) .then(() => ({ item, success: true, err: undefined }))
@ -372,7 +398,7 @@ function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
license: '', license: '',
isSensitive: it.isSensitive, isSensitive: it.isSensitive,
localOnly: false, localOnly: false,
roleIdsThatCanBeUsedThisEmojiAsReaction: '', roleIdsThatCanBeUsedThisEmojiAsReaction: [],
}; };
} }