support update and delete
This commit is contained in:
parent
048e0b8323
commit
84758b6eec
|
@ -50,23 +50,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
|
||||||
computed,
|
|
||||||
defineAsyncComponent,
|
|
||||||
nextTick,
|
|
||||||
onMounted,
|
|
||||||
onUnmounted,
|
|
||||||
ref,
|
|
||||||
shallowRef,
|
|
||||||
toRefs,
|
|
||||||
watch,
|
|
||||||
} from 'vue';
|
|
||||||
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
|
import { GridEventEmitter, GridSetting, Size } from '@/components/grid/grid.js';
|
||||||
import { useTooltip } from '@/scripts/use-tooltip.js';
|
import { useTooltip } from '@/scripts/use-tooltip.js';
|
||||||
import * as os from '@/os.js';
|
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 { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'operation:beginEdit', sender: GridCell): void;
|
(ev: 'operation:beginEdit', sender: GridCell): void;
|
||||||
|
|
|
@ -61,6 +61,7 @@ const props = withDefaults(defineProps<{
|
||||||
}>(), {
|
}>(), {
|
||||||
gridSetting: () => ({
|
gridSetting: () => ({
|
||||||
rowNumberVisible: true,
|
rowNumberVisible: true,
|
||||||
|
rowSelectable: true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const { gridSetting, columnSettings, data } = toRefs(props);
|
const { gridSetting, columnSettings, data } = toRefs(props);
|
||||||
|
@ -576,9 +577,7 @@ function onMouseMove(ev: MouseEvent) {
|
||||||
unSelectionOutOfRange(leftTop, rightBottom);
|
unSelectionOutOfRange(leftTop, rightBottom);
|
||||||
expandCellRange(leftTop, rightBottom);
|
expandCellRange(leftTop, rightBottom);
|
||||||
|
|
||||||
rows.value[targetCellAddress.row].ranged = true;
|
const rangedRowIndexes = [rows.value[targetCellAddress.row].index, ...rangedRows.value.map(it => it.index)];
|
||||||
|
|
||||||
const rangedRowIndexes = rangedRows.value.map(it => it.index);
|
|
||||||
expandRowRange(Math.min(...rangedRowIndexes), Math.max(...rangedRowIndexes));
|
expandRowRange(Math.min(...rangedRowIndexes), Math.max(...rangedRowIndexes));
|
||||||
|
|
||||||
previousCellAddress.value = targetCellAddress;
|
previousCellAddress.value = targetCellAddress;
|
||||||
|
@ -884,6 +883,10 @@ function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
|
||||||
* {@link top}から{@link bottom}までの行を範囲選択状態にする。
|
* {@link top}から{@link bottom}までの行を範囲選択状態にする。
|
||||||
*/
|
*/
|
||||||
function expandRowRange(top: number, bottom: number) {
|
function expandRowRange(top: number, bottom: number) {
|
||||||
|
if (!gridSetting.value.rowSelectable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const targetRows = rows.value.slice(top, bottom + 1);
|
const targetRows = rows.value.slice(top, bottom + 1);
|
||||||
for (const row of targetRows) {
|
for (const row of targetRows) {
|
||||||
row.ranged = true;
|
row.ranged = true;
|
||||||
|
@ -983,11 +986,11 @@ function patchData(newItems: DataSource[]) {
|
||||||
console.log(`[grid][patch-data][begin] new:${newItems.length} old:${cells.value.length}`);
|
console.log(`[grid][patch-data][begin] new:${newItems.length} old:${cells.value.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const _cells = [...cells.value];
|
if (cells.value.length != newItems.length) {
|
||||||
const _rows = [...rows.value];
|
const _cells = [...cells.value];
|
||||||
const _cols = columns.value;
|
const _rows = [...rows.value];
|
||||||
|
const _cols = columns.value;
|
||||||
|
|
||||||
if (_cells.length != newItems.length) {
|
|
||||||
// 状態が壊れるかもしれないので選択を全解除
|
// 状態が壊れるかもしれないので選択を全解除
|
||||||
unSelectionRangeAll();
|
unSelectionRangeAll();
|
||||||
|
|
||||||
|
@ -1026,20 +1029,19 @@ function patchData(newItems: DataSource[]) {
|
||||||
|
|
||||||
rows.value = newRows;
|
rows.value = newRows;
|
||||||
cells.value = newCells;
|
cells.value = newCells;
|
||||||
} else {
|
}
|
||||||
// 行数が変わらない場合
|
|
||||||
for (let rowIdx = 0; rowIdx < _cells.length; rowIdx++) {
|
|
||||||
const oldCells = _cells[rowIdx].cells;
|
|
||||||
const newItem = newItems[rowIdx];
|
|
||||||
for (let colIdx = 0; colIdx < oldCells.length; colIdx++) {
|
|
||||||
const _col = _cols[colIdx];
|
|
||||||
|
|
||||||
const oldCell = oldCells[colIdx];
|
for (let rowIdx = 0; rowIdx < cells.value.length; rowIdx++) {
|
||||||
const newValue = newItem[_col.setting.bindTo];
|
const oldCells = cells.value[rowIdx].cells;
|
||||||
if (oldCell.value !== newValue) {
|
const newItem = newItems[rowIdx];
|
||||||
oldCell.violation = cellValidation(oldCell, newValue);
|
for (let colIdx = 0; colIdx < oldCells.length; colIdx++) {
|
||||||
oldCell.value = newValue;
|
const _col = columns.value[colIdx];
|
||||||
}
|
|
||||||
|
const oldCell = oldCells[colIdx];
|
||||||
|
const newValue = newItem[_col.setting.bindTo];
|
||||||
|
if (oldCell.value !== newValue) {
|
||||||
|
oldCell.violation = cellValidation(oldCell, newValue);
|
||||||
|
oldCell.value = newValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { CellValue } from '@/components/grid/cell.js';
|
||||||
|
|
||||||
export type GridSetting = {
|
export type GridSetting = {
|
||||||
rowNumberVisible: boolean;
|
rowNumberVisible: boolean;
|
||||||
|
rowSelectable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DataSource = Record<string, CellValue>;
|
export type DataSource = Record<string, CellValue>;
|
||||||
|
|
|
@ -12,7 +12,8 @@ export type GridItem = {
|
||||||
readonly fileId?: string;
|
readonly fileId?: string;
|
||||||
readonly url: string;
|
readonly url: string;
|
||||||
|
|
||||||
checked: boolean;
|
deleteCheck: boolean;
|
||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
category: string;
|
category: string;
|
||||||
aliases: string;
|
aliases: string;
|
||||||
|
@ -27,7 +28,7 @@ export function fromEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem
|
||||||
id: it.id,
|
id: it.id,
|
||||||
fileId: undefined,
|
fileId: undefined,
|
||||||
url: it.url,
|
url: it.url,
|
||||||
checked: false,
|
deleteCheck: false,
|
||||||
name: it.name,
|
name: it.name,
|
||||||
category: it.category ?? '',
|
category: it.category ?? '',
|
||||||
aliases: it.aliases.join(', '),
|
aliases: it.aliases.join(', '),
|
||||||
|
@ -43,7 +44,7 @@ export function fromDriveFile(it: Misskey.entities.DriveFile): GridItem {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
fileId: it.id,
|
fileId: it.id,
|
||||||
url: it.url,
|
url: it.url,
|
||||||
checked: false,
|
deleteCheck: false,
|
||||||
name: it.name.replace(/(\.[a-zA-Z0-9]+)+$/, ''),
|
name: it.name.replace(/(\.[a-zA-Z0-9]+)+$/, ''),
|
||||||
category: '',
|
category: '',
|
||||||
aliases: '',
|
aliases: '',
|
||||||
|
|
|
@ -9,10 +9,8 @@
|
||||||
</MkButton>
|
</MkButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;">
|
||||||
style="overflow-y: scroll; padding-top: 8px; padding-bottom: 8px;"
|
<MkGrid :data="gridItems" :gridSetting="gridSetting" :columnSettings="columnSettings" @event="onGridEvent"/>
|
||||||
>
|
|
||||||
<MkGrid :data="gridItems" :columnSettings="columnSettings"/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="_gaps">
|
<div class="_gaps">
|
||||||
|
@ -22,16 +20,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="$style.buttons">
|
<div :class="$style.buttons">
|
||||||
<MkButton primary>{{ i18n.ts.update }}</MkButton>
|
<MkButton primary :disabled="updateButtonDisabled" @click="onUpdateClicked">{{ i18n.ts.update }}</MkButton>
|
||||||
<MkButton>リセット</MkButton>
|
<MkButton @click="onResetClicked">リセット</MkButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref, toRefs, watch } from 'vue';
|
import { computed, ref, toRefs, watch } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import * as os from '@/os.js';
|
||||||
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
import { fromEmojiDetailed, GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
|
@ -39,12 +38,30 @@ import MkInput from '@/components/MkInput.vue';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import { ColumnSetting } from '@/components/grid/column.js';
|
import { ColumnSetting } from '@/components/grid/column.js';
|
||||||
import { validators } from '@/components/grid/cell-validators.js';
|
import { validators } from '@/components/grid/cell-validators.js';
|
||||||
|
import {
|
||||||
|
GridCellContextMenuEvent,
|
||||||
|
GridCellValidationEvent,
|
||||||
|
GridCellValueChangeEvent,
|
||||||
|
GridCurrentState,
|
||||||
|
GridEvent,
|
||||||
|
GridKeyDownEvent,
|
||||||
|
GridRowContextMenuEvent,
|
||||||
|
} from '@/components/grid/grid-event.js';
|
||||||
|
import { optInGridUtils } from '@/components/grid/optin-utils.js';
|
||||||
|
import { GridSetting } from '@/components/grid/grid.js';
|
||||||
|
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||||
|
|
||||||
|
const gridSetting: GridSetting = {
|
||||||
|
rowNumberVisible: true,
|
||||||
|
rowSelectable: false,
|
||||||
|
};
|
||||||
|
|
||||||
const required = validators.required();
|
const required = validators.required();
|
||||||
|
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
|
||||||
const columnSettings: ColumnSetting[] = [
|
const columnSettings: ColumnSetting[] = [
|
||||||
{ bindTo: 'selected', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
|
{ bindTo: 'deleteCheck', icon: 'ti-trash', type: 'boolean', editable: true, width: 34 },
|
||||||
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
|
{ bindTo: 'url', icon: 'ti-icons', type: 'image', editable: false, width: 'auto', validators: [required] },
|
||||||
{ bindTo: 'name', title: 'name', type: 'text', editable: true, width: 140, validators: [required] },
|
{ 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 },
|
||||||
|
@ -65,11 +82,57 @@ const { customEmojis } = toRefs(props);
|
||||||
|
|
||||||
const query = ref('');
|
const query = ref('');
|
||||||
const gridItems = ref<GridItem[]>([]);
|
const gridItems = ref<GridItem[]>([]);
|
||||||
|
const originGridItems = ref<GridItem[]>([]);
|
||||||
|
const updateButtonDisabled = ref<boolean>(false);
|
||||||
|
|
||||||
const latest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[0]?.id : undefined);
|
const latest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[0]?.id : undefined);
|
||||||
const oldest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[customEmojis.value.length - 1]?.id : undefined);
|
const oldest = computed(() => customEmojis.value.length > 0 ? customEmojis.value[customEmojis.value.length - 1]?.id : undefined);
|
||||||
|
|
||||||
watch(customEmojis, refreshGridItems);
|
watch(customEmojis, refreshGridItems, { immediate: true });
|
||||||
|
|
||||||
|
async function onUpdateClicked() {
|
||||||
|
const _items = gridItems.value;
|
||||||
|
const _originItems = originGridItems.value;
|
||||||
|
if (_items.length !== _originItems.length) {
|
||||||
|
throw new Error('The number of items has been changed. Please refresh the page and try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedItems = _items.filter((it, idx) => !it.deleteCheck && JSON.stringify(it) !== JSON.stringify(_originItems[idx]));
|
||||||
|
const deleteItems = _items.filter((it) => it.deleteCheck);
|
||||||
|
|
||||||
|
async function action() {
|
||||||
|
const emptyStrToNull = (value: string) => value === '' ? null : value;
|
||||||
|
const emptyStrToEmptyArray = (value: string) => value === '' ? [] : value.split(',').map(it => it.trim());
|
||||||
|
|
||||||
|
for (const item of updatedItems) {
|
||||||
|
await misskeyApi('admin/emoji/update', {
|
||||||
|
id: item.id!,
|
||||||
|
name: item.name,
|
||||||
|
category: emptyStrToNull(item.category),
|
||||||
|
aliases: emptyStrToEmptyArray(item.aliases),
|
||||||
|
license: emptyStrToNull(item.license),
|
||||||
|
isSensitive: item.isSensitive,
|
||||||
|
localOnly: item.localOnly,
|
||||||
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteIds = deleteItems.map(it => it.id!);
|
||||||
|
await misskeyApi('admin/emoji/delete-bulk', { ids: deleteIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
await os.promiseDialog(
|
||||||
|
action(),
|
||||||
|
() => {},
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
|
||||||
|
emit('operation:search', query.value, undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onResetClicked() {
|
||||||
|
refreshGridItems();
|
||||||
|
}
|
||||||
|
|
||||||
function onSearchButtonClicked() {
|
function onSearchButtonClicked() {
|
||||||
emit('operation:search', query.value, undefined, undefined);
|
emit('operation:search', query.value, undefined, undefined);
|
||||||
|
@ -83,13 +146,73 @@ async function onOldestButtonClicked() {
|
||||||
emit('operation:search', query.value, undefined, oldest.value);
|
emit('operation:search', query.value, undefined, oldest.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshGridItems() {
|
function onGridEvent(event: GridEvent, currentState: GridCurrentState) {
|
||||||
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
|
switch (event.type) {
|
||||||
|
case 'cell-validation':
|
||||||
|
onGridCellValidation(event);
|
||||||
|
break;
|
||||||
|
case 'row-context-menu':
|
||||||
|
onGridRowContextMenu(event, currentState);
|
||||||
|
break;
|
||||||
|
case 'cell-context-menu':
|
||||||
|
onGridCellContextMenu(event, currentState);
|
||||||
|
break;
|
||||||
|
case 'cell-value-change':
|
||||||
|
onGridCellValueChange(event, currentState);
|
||||||
|
break;
|
||||||
|
case 'keydown':
|
||||||
|
onGridKeyDown(event, currentState);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
function onGridCellValidation(event: GridCellValidationEvent) {
|
||||||
refreshGridItems();
|
updateButtonDisabled.value = event.all.filter(it => !it.valid).length > 0;
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function onGridRowContextMenu(event: GridRowContextMenuEvent, currentState: GridCurrentState) {
|
||||||
|
event.menuItems.push(
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '選択行をコピー',
|
||||||
|
icon: 'ti ti-copy',
|
||||||
|
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridCellContextMenu(event: GridCellContextMenuEvent, currentState: GridCurrentState) {
|
||||||
|
event.menuItems.push(
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '選択範囲をコピー',
|
||||||
|
icon: 'ti ti-copy',
|
||||||
|
action: () => optInGridUtils.copyToClipboard(gridItems, currentState),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'button',
|
||||||
|
text: '選択行を削除',
|
||||||
|
icon: 'ti ti-trash',
|
||||||
|
action: () => optInGridUtils.deleteSelectionRange(gridItems, currentState),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridCellValueChange(event: GridCellValueChangeEvent, currentState: GridCurrentState) {
|
||||||
|
const { row, column, newValue } = event;
|
||||||
|
if (gridItems.value.length > row.index && column.setting.bindTo in gridItems.value[row.index]) {
|
||||||
|
gridItems.value[row.index][column.setting.bindTo] = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGridKeyDown(event: GridKeyDownEvent, currentState: GridCurrentState) {
|
||||||
|
optInGridUtils.defaultKeyDownHandler(gridItems, event, currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshGridItems() {
|
||||||
|
gridItems.value = customEmojis.value.map(it => fromEmojiDetailed(it));
|
||||||
|
originGridItems.value = JSON.parse(JSON.stringify(gridItems.value));
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -154,17 +154,20 @@ async function onRegistryClicked() {
|
||||||
|
|
||||||
const items = new Map<string, GridItem>(gridItems.value.map(it => [`${it.fileId}|${it.name}`, it]));
|
const items = new Map<string, GridItem>(gridItems.value.map(it => [`${it.fileId}|${it.name}`, it]));
|
||||||
const upload = async (): Promise<UploadResult[]> => {
|
const upload = async (): Promise<UploadResult[]> => {
|
||||||
|
const emptyStrToNull = (value: string) => value === '' ? null : value;
|
||||||
|
const emptyStrToEmptyArray = (value: string) => value === '' ? [] : value.split(',').map(it => it.trim());
|
||||||
|
|
||||||
const result = Array.of<UploadResult>();
|
const result = Array.of<UploadResult>();
|
||||||
for (const [key, item] of [...items.entries()].slice(0, MAXIMUM_EMOJI_COUNT)) {
|
for (const [key, item] of [...items.entries()].slice(0, MAXIMUM_EMOJI_COUNT)) {
|
||||||
try {
|
try {
|
||||||
await misskeyApi('admin/emoji/add', {
|
await misskeyApi('admin/emoji/add', {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
category: item.category,
|
category: emptyStrToNull(item.category),
|
||||||
aliases: item.aliases.split(',').map(it => it.trim()),
|
aliases: emptyStrToEmptyArray(item.aliases),
|
||||||
license: item.license,
|
license: emptyStrToNull(item.license),
|
||||||
isSensitive: item.isSensitive,
|
isSensitive: item.isSensitive,
|
||||||
localOnly: item.localOnly,
|
localOnly: item.localOnly,
|
||||||
roleIdsThatCanBeUsedThisEmojiAsReaction: item.roleIdsThatCanBeUsedThisEmojiAsReaction.split(',').map(it => it.trim()),
|
roleIdsThatCanBeUsedThisEmojiAsReaction: emptyStrToEmptyArray(item.roleIdsThatCanBeUsedThisEmojiAsReaction),
|
||||||
fileId: item.fileId!,
|
fileId: item.fileId!,
|
||||||
});
|
});
|
||||||
result.push({ key, item, success: true, err: undefined });
|
result.push({ key, item, success: true, err: undefined });
|
||||||
|
|
Loading…
Reference in New Issue