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

View file

@ -97,6 +97,14 @@ export class EmojiEntityService {
...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 {

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 { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
import { GridRowSetting } from '@/components/grid/row.js';
import { selectFile } from '@/scripts/select-file.js';
const emit = defineEmits<{
(ev: 'operation:beginEdit', sender: GridCell): void;
@ -107,7 +106,7 @@ watch(() => cell.value.selected, () => {
function onCellDoubleClick(ev: MouseEvent) {
switch (ev.type) {
case 'dblclick': {
beginEditing();
beginEditing(ev.target as HTMLElement);
break;
}
}
@ -127,7 +126,7 @@ function onCellKeyDown(ev: KeyboardEvent) {
case 'NumpadEnter':
case 'Enter':
case 'F2': {
beginEditing();
beginEditing(ev.target as HTMLElement);
break;
}
}
@ -164,37 +163,47 @@ function unregisterOutsideMouseDown() {
removeEventListener('mousedown', onOutsideMouseDown);
}
async function beginEditing() {
async function beginEditing(target: HTMLElement) {
if (editing.value || !cell.value.column.setting.editable) {
return;
}
switch (cellType.value) {
case 'text': {
editingValue.value = cell.value.value;
editing.value = true;
registerOutsideMouseDown();
emit('operation:beginEdit', cell.value);
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);
await nextTick(() => {
// input
if (inputAreaEl.value) {
(inputAreaEl.value.querySelector('*') as HTMLElement).focus();
}
});
break;
if (newValue !== cell.value.value) {
emitValueChange(newValue);
}
case 'boolean': {
// UI
emitValueChange(!cell.value.value);
break;
}
case 'image': {
const file = await selectFile(rootEl.value);
if (file) {
emitValueChange(JSON.stringify(file));
rootEl.value?.focus();
} else {
switch (cellType.value) {
case 'text': {
editingValue.value = cell.value.value;
editing.value = true;
registerOutsideMouseDown();
emit('operation:beginEdit', cell.value);
await nextTick(() => {
// input
if (inputAreaEl.value) {
(inputAreaEl.value.querySelector('*') as HTMLElement).focus();
}
});
break;
}
case 'boolean': {
// UI
emitValueChange(!cell.value.value);
break;
}
break;
}
}
}

View file

@ -1170,7 +1170,9 @@ function patchData(newItems: DataSource[]) {
const newValue = newItem[_col.setting.bindTo];
if (oldCell.value !== 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);
}
}
@ -1199,6 +1201,8 @@ function patchData(newItems: DataSource[]) {
// #endregion
onMounted(() => {
state.value = 'normal';
const bindToList = columnSettings.map(it => it.bindTo);
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 { 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 = {
row: number;
@ -41,9 +41,13 @@ export function createCell(
value: CellValue,
setting: GridCellSetting,
): GridCell {
const newValue = (row.using && column.setting.valueTransformer)
? column.setting.valueTransformer(row, column, value)
: value;
return {
address: { row: row.index, col: column.index },
value,
value: newValue,
column,
row,
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 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 GridColumnSetting = {
@ -19,7 +20,8 @@ export type GridColumnSetting = {
width: SizeStyle;
editable?: boolean;
validators?: CellValidator[];
valueConverter?: CellValueConverter;
customValueEditor?: CustomValueEditor;
valueTransformer?: CellValueTransformer;
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 bounds = context.randedBounds;
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++) {
const bindTo = context.columns[col].setting.bindTo;
const cell = gridItems.value[row][bindTo];
items.push(cell?.toString() ?? '');
const cell = items[row][bindTo];
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');
@ -66,9 +70,12 @@ class OptInGridUtils {
async pasteFromClipboard(
gridItems: Ref<DataSource[]>,
context: GridContext,
valueConverters?: { bindTo: string, converter: (value: string) => CellValue }[],
) {
function parseValue(value: string, type: GridColumnSetting['type']): CellValue {
switch (type) {
const converterMap = new Map<string, (value: string) => CellValue>(valueConverters?.map(it => [it.bindTo, it.converter]) ?? []);
function parseValue(value: string, setting: GridColumnSetting): CellValue {
switch (setting.type) {
case 'number': {
return Number(value);
}
@ -76,7 +83,9 @@ class OptInGridUtils {
return value === 'true';
}
default: {
return value;
return converterMap.has(setting.bindTo)
? converterMap.get(setting.bindTo)!(value)
: value;
}
}
}
@ -95,7 +104,7 @@ class OptInGridUtils {
// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
const ranges = context.rangedCells;
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 {
// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
@ -117,13 +126,13 @@ class OptInGridUtils {
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) {
const deletedIndexes = context.rangedRows.map(it => it.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) {
return new Promise((resolve, reject) => {
popup(MkEmojiPickerDialog, {

View file

@ -56,6 +56,18 @@
<MkInput v-model="queryUpdatedAtTo" :debounce="true" type="date" autocapitalize="off" class="col3 row3">
<template #label>updatedAt(to)</template>
</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>
<MkFolder :spacerMax="8" :spacerMin="8">
@ -153,6 +165,8 @@ import { deviceKind } from '@/scripts/device-kind.js';
import { GridSetting } from '@/components/grid/grid.js';
import MkTagItem from '@/components/MkTagItem.vue';
import { MenuItem } from '@/types/menu.js';
import { CellValue } from '@/components/grid/cell.js';
import { selectFile } from '@/scripts/select-file.js';
type GridItem = {
checked: boolean;
@ -165,7 +179,7 @@ type GridItem = {
license: string;
isSensitive: boolean;
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
fileId?: string;
updatedAt: string | null;
publicUrl?: string | null;
@ -230,14 +244,54 @@ function setupGrid(): GridSetting {
},
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: '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: '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: '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: 'publicUrl', type: 'text', editable: false, width: 180 },
{ bindTo: 'originalUrl', type: 'text', editable: false, width: 180 },
@ -249,7 +303,9 @@ function setupGrid(): GridSetting {
type: 'button',
text: '選択範囲をコピー',
icon: 'ti ti-copy',
action: () => optInGridUtils.copyToClipboard(gridItems, context),
action: () => {
return optInGridUtils.copyToClipboard(gridItems, context);
},
},
{
type: 'button',
@ -286,6 +342,7 @@ const queryUpdatedAtFrom = ref<string | null>(null);
const queryUpdatedAtTo = ref<string | null>(null);
const querySensitive = ref<string | null>(null);
const queryLocalOnly = ref<string | null>(null);
const queryRoles = ref<{ id: string, name: string }[]>([]);
const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<GridSortOrder[]>([]);
const requestLogs = ref<RequestLogItem[]>([]);
@ -295,6 +352,7 @@ const originGridItems = ref<GridItem[]>([]);
const updateButtonDisabled = ref<boolean>(false);
const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind));
const queryRolesText = computed(() => queryRoles.value.map(it => it.name).join(','));
async function onUpdateButtonClicked() {
const _items = gridItems.value;
@ -334,7 +392,7 @@ async function onUpdateButtonClicked() {
license: emptyStrToNull(item.license),
isSensitive: item.isSensitive,
localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
fileId: item.fileId,
})
.then(() => ({ item, success: true, err: undefined }))
@ -402,6 +460,19 @@ function onGridResetButtonClicked() {
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) {
console.log(order);
switch (order.direction) {
@ -446,6 +517,7 @@ function onQueryResetButtonClicked() {
queryUpdatedAtTo.value = null;
querySensitive.value = null;
queryLocalOnly.value = null;
queryRoles.value = [];
}
async function onPageChanged(pageNumber: number) {
@ -474,17 +546,27 @@ function onGridCellValidation(event: GridCellValidationEvent) {
function onGridCellValueChange(event: GridCellValueChangeEvent) {
const { row, column, newValue } = event;
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) {
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;
switch (true) {
@ -498,7 +580,13 @@ async function onGridKeyDown(event: GridKeyDownEvent, currentState: GridContext)
break;
}
case 'KeyV': {
await optInGridUtils.pasteFromClipboard(gridItems, currentState);
await optInGridUtils.pasteFromClipboard(
gridItems,
currentState,
[
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', converter: roleIdConverter },
],
);
break;
}
}
@ -543,6 +631,7 @@ async function refreshCustomEmojis() {
localOnly: queryLocalOnly.value ? Boolean(queryLocalOnly.value).valueOf() : undefined,
updatedAtFrom: emptyStrToUndefined(queryUpdatedAtFrom.value),
updatedAtTo: emptyStrToUndefined(queryUpdatedAtTo.value),
roleIds: queryRoles.value.map(it => it.id),
hostType: 'local',
};
@ -584,7 +673,7 @@ function refreshGridItems() {
license: it.license ?? '',
isSensitive: it.isSensitive,
localOnly: it.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(','),
roleIdsThatCanBeUsedThisEmojiAsReaction: it.roleIdsThatCanBeUsedThisEmojiAsReaction,
updatedAt: it.updatedAt,
publicUrl: it.publicUrl,
originalUrl: it.originalUrl,

View file

@ -116,7 +116,7 @@ type GridItem = {
license: string;
isSensitive: boolean;
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: string;
roleIdsThatCanBeUsedThisEmojiAsReaction: { id: string, name: string }[];
}
function setupGrid(): GridSetting {
@ -159,7 +159,33 @@ function setupGrid(): GridSetting {
{ 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 },
{
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: {
contextMenuFactory: (col, row, value, context) => {
@ -214,7 +240,7 @@ async function onRegistryClicked() {
license: emptyStrToNull(item.license),
isSensitive: item.isSensitive,
localOnly: item.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.map(it => it.id),
fileId: item.fileId!,
})
.then(() => ({ item, success: true, err: undefined }))
@ -372,7 +398,7 @@ function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
license: '',
isSensitive: it.isSensitive,
localOnly: false,
roleIdsThatCanBeUsedThisEmojiAsReaction: '',
roleIdsThatCanBeUsedThisEmojiAsReaction: [],
};
}