mirror of
https://github.com/misskey-dev/misskey.git
synced 2025-01-01 04:56:19 +01:00
wip
This commit is contained in:
parent
457a0a19ec
commit
9494c30c9f
8 changed files with 231 additions and 135 deletions
|
@ -53,9 +53,9 @@
|
|||
"json5": "2.2.3",
|
||||
"matter-js": "0.19.0",
|
||||
"mfm-js": "0.24.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"misskey-reversi": "workspace:*",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"photoswipe": "5.4.3",
|
||||
"punycode": "2.3.1",
|
||||
"rollup": "4.9.1",
|
||||
|
@ -115,8 +115,6 @@
|
|||
"@vitest/coverage-v8": "0.34.6",
|
||||
"@vue/runtime-core": "3.4.3",
|
||||
"acorn": "8.11.2",
|
||||
"ag-grid-community": "^31.0.2",
|
||||
"ag-grid-vue3": "^31.0.2",
|
||||
"blueimp-load-image": "^5.16.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "13.6.1",
|
||||
|
|
26
packages/frontend/src/components/grid/MkCell.vue
Normal file
26
packages/frontend/src/components/grid/MkCell.vue
Normal file
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<div
|
||||
class="cell"
|
||||
:style="{ width: width + 'px' }"
|
||||
>
|
||||
<span>{{ cell.value }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from 'vue';
|
||||
import { GridCell } from '@/components/grid/types.js';
|
||||
|
||||
const props = defineProps<{ cell: GridCell }>();
|
||||
const { cell } = toRefs(props);
|
||||
const width = cell.value.columnSetting.width;
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.cell {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
98
packages/frontend/src/components/grid/MkGrid.vue
Normal file
98
packages/frontend/src/components/grid/MkGrid.vue
Normal file
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<div :style="$style.grid">
|
||||
<div :class="$style.header">
|
||||
<div
|
||||
v-for="column in columns"
|
||||
:key="column.index"
|
||||
:class="$style.cell"
|
||||
:style="{ width: column.setting.width + 'px'}"
|
||||
>
|
||||
{{ column.setting.title ?? column.setting.bindTo }}
|
||||
</div>
|
||||
</div>
|
||||
<MkRow v-for="row in rows" :key="row.index" :row="row"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
import MkRow from '@/components/grid/MkRow.vue';
|
||||
import { ColumnSetting, DataSource, GridCell, GridColumn, GridRow } from '@/components/grid/types.js';
|
||||
|
||||
const props = defineProps<{
|
||||
columnSettings: ColumnSetting[],
|
||||
data: DataSource[]
|
||||
}>();
|
||||
|
||||
const { columnSettings, data } = toRefs(props);
|
||||
const columns = ref<GridColumn[]>();
|
||||
const rows = ref<GridRow[]>();
|
||||
|
||||
watch(columnSettings, refreshColumnsSetting);
|
||||
watch(data, refreshData);
|
||||
|
||||
function refreshColumnsSetting() {
|
||||
const bindToList = columnSettings.value.map(it => it.bindTo);
|
||||
if (new Set(bindToList).size !== columnSettings.value.length) {
|
||||
throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
|
||||
}
|
||||
|
||||
refreshData();
|
||||
}
|
||||
|
||||
function refreshData() {
|
||||
const _settings = columnSettings.value;
|
||||
const _data = data.value;
|
||||
const _rows = _data.map((_, index) => ({ index, cells: Array.of<GridCell>() }));
|
||||
const _columns = columnSettings.value.map((setting, index) => ({ index, setting, cells: Array.of<GridCell>() }));
|
||||
|
||||
for (const [rowIndex, row] of _rows.entries()) {
|
||||
for (const [colIndex, column] of _columns.entries()) {
|
||||
if (!(column.setting.bindTo in _data[rowIndex])) {
|
||||
continue;
|
||||
}
|
||||
const value = _data[rowIndex][column.setting.bindTo];
|
||||
|
||||
const cell = {
|
||||
address: { col: colIndex, row: rowIndex },
|
||||
value,
|
||||
columnSetting: _settings[colIndex],
|
||||
};
|
||||
|
||||
row.cells.push(cell);
|
||||
column.cells.push(cell);
|
||||
}
|
||||
}
|
||||
|
||||
rows.value = _rows;
|
||||
columns.value = _columns;
|
||||
}
|
||||
|
||||
refreshColumnsSetting();
|
||||
refreshData();
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: fit-content;
|
||||
|
||||
> .cell {
|
||||
display: block;
|
||||
padding: 4px 8px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
23
packages/frontend/src/components/grid/MkRow.vue
Normal file
23
packages/frontend/src/components/grid/MkRow.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div :class="$style.row">
|
||||
<div v-for="cell in row.cells" :key="JSON.stringify(cell.address)">
|
||||
<MkCell :cell="cell"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MkCell from '@/components/grid/MkCell.vue';
|
||||
import { GridRow } from '@/components/grid/types.js';
|
||||
|
||||
defineProps<{ row: GridRow }>();
|
||||
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
</style>
|
35
packages/frontend/src/components/grid/types.ts
Normal file
35
packages/frontend/src/components/grid/types.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
export type CellValue = string | boolean | number | undefined | null
|
||||
|
||||
export type CellAddress = {
|
||||
row: number;
|
||||
col: number;
|
||||
}
|
||||
|
||||
export type GridCell = {
|
||||
address: CellAddress;
|
||||
value: CellValue;
|
||||
columnSetting: ColumnSetting;
|
||||
}
|
||||
|
||||
export type ColumnType = 'text' | 'number' | 'date' | 'boolean' | 'image';
|
||||
|
||||
export type ColumnSetting = {
|
||||
bindTo: string;
|
||||
title?: string;
|
||||
type: ColumnType;
|
||||
width?: number | 'auto';
|
||||
editable?: boolean;
|
||||
};
|
||||
|
||||
export type DataSource = Record<string, CellValue>;
|
||||
|
||||
export type GridColumn = {
|
||||
index: number;
|
||||
setting: ColumnSetting;
|
||||
cells: GridCell[];
|
||||
}
|
||||
|
||||
export type GridRow = {
|
||||
index: number;
|
||||
cells: GridCell[];
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="root">
|
||||
<img class="thumbnail" :src="emojiUrl" :alt="emojiName"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
import { CellClassParams } from 'ag-grid-community';
|
||||
import { computed } from 'vue';
|
||||
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||
|
||||
const props = defineProps<{
|
||||
params: CellClassParams<GridItem, string>
|
||||
}>();
|
||||
|
||||
const emojiUrl = computed(() => props.params.data?.url);
|
||||
const emojiName = computed(() => props.params.data?.name);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
object-fit: cover;
|
||||
width: 26px;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -5,9 +5,9 @@ export class GridItem {
|
|||
readonly url?: string;
|
||||
readonly blob?: Blob;
|
||||
|
||||
public aliases: string;
|
||||
public name: string;
|
||||
public category: string;
|
||||
public aliases: string;
|
||||
public license: string;
|
||||
public isSensitive: boolean;
|
||||
public localOnly: boolean;
|
||||
|
@ -15,13 +15,13 @@ export class GridItem {
|
|||
|
||||
private readonly origin: string;
|
||||
|
||||
private constructor(
|
||||
constructor(
|
||||
id: string | undefined,
|
||||
url: string | undefined = undefined,
|
||||
blob: Blob | undefined = undefined,
|
||||
aliases: string,
|
||||
name: string,
|
||||
category: string,
|
||||
aliases: string,
|
||||
license: string,
|
||||
isSensitive: boolean,
|
||||
localOnly: boolean,
|
||||
|
@ -43,22 +43,15 @@ export class GridItem {
|
|||
}
|
||||
|
||||
static ofEmojiDetailed(it: Misskey.entities.EmojiDetailed): GridItem {
|
||||
return new GridItem(
|
||||
it.id,
|
||||
it.url,
|
||||
undefined,
|
||||
it.aliases.join(', '),
|
||||
it.name,
|
||||
it.category ?? '',
|
||||
it.license ?? '',
|
||||
it.isSensitive,
|
||||
it.localOnly,
|
||||
it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '),
|
||||
);
|
||||
return new GridItem(it.id, it.url, undefined, it.name, it.category ?? '', it.aliases.join(', '), it.license ?? '', it.isSensitive, it.localOnly, it.roleIdsThatCanBeUsedThisEmojiAsReaction.join(', '));
|
||||
}
|
||||
|
||||
public get edited(): boolean {
|
||||
const { origin, ..._this } = this;
|
||||
return JSON.stringify(_this) !== origin;
|
||||
}
|
||||
|
||||
public asRecord(): Record<string, unknown> {
|
||||
return this as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,104 +5,67 @@
|
|||
<MkPageHeader/>
|
||||
</template>
|
||||
<div class="_gaps" :class="$style.root">
|
||||
<AgGridVue
|
||||
:rowData="gridItems"
|
||||
:columnDefs="colDefs"
|
||||
:rowSelection="'multiple'"
|
||||
:rowClassRules="rowClassRules"
|
||||
style="height: 500px"
|
||||
class="ag-theme-quartz-auto-dark"
|
||||
@gridReady="onGridReady"
|
||||
>
|
||||
</AgGridVue>
|
||||
<MkGrid :data="gridItems" :columnSettings="columnSettings"/>
|
||||
</div>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { markRaw, onMounted, ref, watch } from 'vue';
|
||||
import 'ag-grid-community/styles/ag-grid.css';
|
||||
import 'ag-grid-community/styles/ag-theme-quartz.css';
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { AgGridVue } from 'ag-grid-vue3';
|
||||
import { ColDef, GridApi, GridReadyEvent, RowClassRules } from 'ag-grid-community';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { GridItem } from '@/pages/admin/custom-emojis-grid.impl.js';
|
||||
import CustomEmojisGridEmoji from '@/pages/admin/custom-emojis-grid-emoji.vue';
|
||||
import MkGrid from '@/components/grid/MkGrid.vue';
|
||||
import { ColumnSetting } from '@/components/grid/types.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default {
|
||||
components: {
|
||||
AgGridVue,
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
customEmojisGridEmoji: CustomEmojisGridEmoji,
|
||||
},
|
||||
setup() {
|
||||
const colDefs = markRaw<ColDef[]>([
|
||||
{
|
||||
field: 'img',
|
||||
headerName: '',
|
||||
initialWidth: 90,
|
||||
cellRenderer: 'customEmojisGridEmoji',
|
||||
checkboxSelection: true,
|
||||
},
|
||||
{ field: 'name', headerName: 'name', initialWidth: 140, editable: true },
|
||||
{ field: 'category', headerName: 'category', initialWidth: 140, editable: true },
|
||||
{ field: 'aliases', headerName: 'aliases', initialWidth: 140, editable: true },
|
||||
{ field: 'license', headerName: 'license', initialWidth: 140, editable: true },
|
||||
{ field: 'isSensitive', headerName: 'sensitive', initialWidth: 90, editable: true },
|
||||
{ field: 'localOnly', headerName: 'localOnly', initialWidth: 90, editable: true },
|
||||
{ field: 'roleIdsThatCanBeUsedThisEmojiAsReaction', headerName: 'role', initialWidth: 140, editable: true },
|
||||
]);
|
||||
const columnSettings: ColumnSetting[] = [
|
||||
{ bindTo: 'url', title: ' ', type: 'image', width: 50 },
|
||||
{ bindTo: 'name', title: 'name', type: 'text', width: 140 },
|
||||
{ bindTo: 'category', title: 'category', type: 'text', width: 140 },
|
||||
{ bindTo: 'aliases', title: 'aliases', type: 'text', width: 140 },
|
||||
{ bindTo: 'license', title: 'license', type: 'text', width: 140 },
|
||||
{ bindTo: 'isSensitive', title: 'sensitive', type: 'text', width: 90 },
|
||||
{ bindTo: 'localOnly', title: 'localOnly', type: 'text', width: 90 },
|
||||
{ bindTo: 'roleIdsThatCanBeUsedThisEmojiAsReaction', title: 'role', type: 'text', width: 140 },
|
||||
];
|
||||
|
||||
const rowClassRules = markRaw<RowClassRules<GridItem>>({
|
||||
'emoji-grid-row-edited': params => params.data?.edited ?? false,
|
||||
});
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
const gridItems = ref<GridItem[]>([]);
|
||||
// const convertedGridItems = computed<DataSource[]>(() => gridItems.value.map(it => it.asRecord()));
|
||||
|
||||
const gridApi = ref<GridApi>();
|
||||
const customEmojis = ref<Misskey.entities.EmojiDetailed[]>([]);
|
||||
const gridItems = ref<GridItem[]>([]);
|
||||
|
||||
const refreshCustomEmojis = async () => {
|
||||
customEmojis.value = await misskeyApi('emojis', { detail: true }).then(it => it.emojis);
|
||||
};
|
||||
|
||||
const refreshGridItems = () => {
|
||||
console.log(customEmojis.value);
|
||||
gridItems.value = customEmojis.value.map(it => GridItem.ofEmojiDetailed(it));
|
||||
};
|
||||
|
||||
watch(customEmojis, refreshGridItems);
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshCustomEmojis();
|
||||
refreshGridItems();
|
||||
});
|
||||
|
||||
function onGridReady(params: GridReadyEvent) {
|
||||
gridApi.value = params.api;
|
||||
}
|
||||
|
||||
return {
|
||||
colDefs,
|
||||
rowClassRules,
|
||||
customEmojis,
|
||||
gridItems,
|
||||
onGridReady,
|
||||
};
|
||||
},
|
||||
const refreshCustomEmojis = async () => {
|
||||
customEmojis.value = await misskeyApi('emojis', { detail: true }).then(it => it.emojis);
|
||||
};
|
||||
|
||||
const refreshGridItems = () => {
|
||||
gridItems.value = customEmojis.value.map(it => GridItem.ofEmojiDetailed(it));
|
||||
};
|
||||
|
||||
watch(customEmojis, refreshGridItems);
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshCustomEmojis();
|
||||
refreshGridItems();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.emoji-grid-row-edited {
|
||||
background-color: var(--ag-advanced-filter-column-pill-color);
|
||||
}
|
||||
|
||||
.emoji-grid-item-image {
|
||||
width: auto;
|
||||
height: 26px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style module lang="scss">
|
||||
.root {
|
||||
padding: 16px
|
||||
padding: 16px;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue