mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-29 09:58:36 +01:00
support role select
This commit is contained in:
parent
c0f941689b
commit
07b9757b36
11 changed files with 433 additions and 81 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
177
packages/frontend/src/components/MkRoleSelectDialog.vue
Normal file
177
packages/frontend/src/components/MkRoleSelectDialog.vue
Normal 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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
// 取得元のプロパティ名重複は許容したくない
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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, {
|
||||
|
|
|
@ -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) => {
|
||||
// バックエンドからからはIDと名前のペア配列で受け取るが、表示にIDがあると煩雑なので名前だけにする
|
||||
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,
|
||||
|
|
|
@ -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) => {
|
||||
// バックエンドからからはIDと名前のペア配列で受け取るが、表示にIDがあると煩雑なので名前だけにする
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue