summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-01-26 20:10:22 +0900
committerGitHub <noreply@github.com>2025-01-26 11:10:22 +0000
commit297186e492b20c3e54f8cbfe51ec2d7694ca7068 (patch)
tree89607c3f0d68b75834b008b449873e06982792e0 /packages/frontend/src
parentfix(frontend): 画面を閉じる直前にAudioContextを閉じるように (... (diff)
downloadmisskey-297186e492b20c3e54f8cbfe51ec2d7694ca7068.tar.gz
misskey-297186e492b20c3e54f8cbfe51ec2d7694ca7068.tar.bz2
misskey-297186e492b20c3e54f8cbfe51ec2d7694ca7068.zip
enhance(frontend): 絵文字管理画面β(ローカル)のUI・UX改善 (#15349)
* enhance(frontend): 絵文字管理画面β(ローカル)のUI・UX改善 * fix * :art: * 表示件数をメニューから変更するように * 確認ダイアログ * fix i18n * needWideArea: trueならwidgetの開閉ボタンを表示しないように * fix: 検索ウィンドウは一つしか開けないように
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkSortOrderEditor.vue8
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue2
-rw-r--r--packages/frontend/src/components/MkTagItem.vue4
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue12
-rw-r--r--packages/frontend/src/components/grid/MkDataCell.vue35
-rw-r--r--packages/frontend/src/components/grid/MkGrid.vue56
-rw-r--r--packages/frontend/src/components/grid/MkHeaderCell.vue6
-rw-r--r--packages/frontend/src/components/grid/grid.ts5
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts3
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue39
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue213
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue469
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue16
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.local.vue39
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue (renamed from packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue)56
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue26
-rw-r--r--packages/frontend/src/pages/admin/custom-emojis-manager2.vue13
-rw-r--r--packages/frontend/src/pages/admin/index.vue11
-rw-r--r--packages/frontend/src/pages/settings/index.vue2
-rw-r--r--packages/frontend/src/ui/universal.vue2
20 files changed, 632 insertions, 385 deletions
diff --git a/packages/frontend/src/components/MkSortOrderEditor.vue b/packages/frontend/src/components/MkSortOrderEditor.vue
index da08f12297..9decacc5f5 100644
--- a/packages/frontend/src/components/MkSortOrderEditor.vue
+++ b/packages/frontend/src/components/MkSortOrderEditor.vue
@@ -12,12 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
:iconClass="order.direction === '+' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'"
:exButtonIconClass="'ti ti-x'"
:content="order.key"
+ :class="$style.sortOrderTag"
@click="onToggleSortOrderButtonClicked(order)"
@exButtonClick="onRemoveSortOrderButtonClicked(order)"
/>
</div>
<MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked">
- <span class="ti ti-plus"/>
+ <span class="ti ti-plus"></span>
</MkButton>
</div>
</template>
@@ -109,4 +110,9 @@ function emitOrder(sortOrders: SortOrder<T>[]) {
border-radius: 9999px;
background-color: var(--MI_THEME-buttonBg);
}
+
+.sortOrderTag {
+ user-select: none;
+ cursor: pointer;
+}
</style>
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 0caaed6f39..fa0e40d8f9 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -47,7 +47,7 @@ export type SuperMenuDef = {
active?: boolean;
action: (ev: MouseEvent) => void;
} | {
- type: 'link';
+ type?: 'link';
to: string;
icon?: string;
text: string;
diff --git a/packages/frontend/src/components/MkTagItem.vue b/packages/frontend/src/components/MkTagItem.vue
index 98f2411392..8b7460f3a3 100644
--- a/packages/frontend/src/components/MkTagItem.vue
+++ b/packages/frontend/src/components/MkTagItem.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root" @click="(ev) => emit('click', ev)">
- <span v-if="iconClass" :class="[$style.icon, iconClass]"/>
+ <span v-if="iconClass" :class="[$style.icon, iconClass]"></span>
<span :class="$style.content">{{ content }}</span>
<MkButton v-if="exButtonIconClass" :class="$style.exButton" @click="(ev) => emit('exButtonClick', ev)">
- <span :class="[$style.exButtonIcon, exButtonIconClass]"/>
+ <span :class="[$style.exButtonIcon, exButtonIconClass]"></span>
</MkButton>
</div>
</template>
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index aa4be69b2c..a2e70a5cad 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -48,13 +48,16 @@ import { scrollToTop } from '@@/js/scroll.js';
import { globalEvents } from '@/events.js';
import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
-import { PageHeaderItem } from '@/types/page-header.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
+import type { PageMetadata } from '@/scripts/page-metadata.js';
const props = withDefaults(defineProps<{
+ overridePageMetadata?: PageMetadata;
tabs?: Tab[];
tab?: string;
actions?: PageHeaderItem[] | null;
thin?: boolean;
+ hideTitle?: boolean;
displayMyAvatar?: boolean;
}>(), {
tabs: () => ([] as Tab[]),
@@ -64,9 +67,10 @@ const emit = defineEmits<{
(ev: 'update:tab', key: string);
}>();
-const pageMetadata = injectReactiveMetadata();
+const injectedPageMetadata = injectReactiveMetadata();
+const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
-const hideTitle = inject('shouldOmitHeaderTitle', false);
+const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle);
const thin_ = props.thin || inject('shouldHeaderThin', false);
const el = shallowRef<HTMLElement | undefined>(undefined);
@@ -75,7 +79,7 @@ const narrow = ref(false);
const hasTabs = computed(() => props.tabs.length > 0);
const hasActions = computed(() => props.actions && props.actions.length > 0);
const show = computed(() => {
- return !hideTitle || hasTabs.value || hasActions.value;
+ return !hideTitle.value || hasTabs.value || hasActions.value;
});
const preventDrag = (ev: TouchEvent) => {
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
index 0ffd42abda..e473b7c1af 100644
--- a/packages/frontend/src/components/grid/MkDataCell.vue
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -39,13 +39,15 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ cell.value }}
</div>
<div v-else-if="cellType === 'boolean'">
- <span v-if="cell.value === true" class="ti ti-check"/>
- <span v-else class="ti"/>
+ <div :class="[$style.bool, {
+ [$style.boolTrue]: cell.value === true,
+ 'ti ti-check': cell.value === true,
+ }]"></div>
</div>
<div v-else-if="cellType === 'image'">
<img
- :src="cell.value as string"
- :alt="cell.value as string"
+ :src="cell.value"
+ :alt="cell.value"
:class="$style.viewImage"
@load="emitContentSizeChanged"
/>
@@ -375,6 +377,31 @@ $cellHeight: 28px;
object-fit: cover;
}
+.bool {
+ position: relative;
+ width: 18px;
+ height: 18px;
+ background: var(--MI_THEME-panel);
+ border: solid 2px var(--MI_THEME-divider);
+ border-radius: 4px;
+ box-sizing: border-box;
+
+ &.boolTrue {
+ border-color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accent);
+
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ color: var(--MI_THEME-fgOnAccent);
+ font-size: 12px;
+ line-height: 18px;
+ }
+ }
+}
+
.editingInput {
padding: 0 8px;
width: 100%;
diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
index 60738365fb..4dbd4ebcae 100644
--- a/packages/frontend/src/components/grid/MkGrid.vue
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -7,7 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<div
ref="rootEl"
class="mk_grid_border"
- :class="[$style.grid]"
+ :class="[$style.grid, {
+ [$style.noOverflowHandling]: rootSetting.noOverflowStyle,
+ 'mk_grid_root_rounded': rootSetting.rounded,
+ 'mk_grid_root_border': rootSetting.outerBorder,
+ }]"
@mousedown.prevent="onMouseDown"
@keydown="onKeyDown"
@contextmenu.prevent.stop="onContextMenu"
@@ -77,10 +81,17 @@ const emit = defineEmits<{
}>();
const props = defineProps<{
- settings: GridSetting,
- data: DataSource[]
+ settings: GridSetting;
+ data: DataSource[];
}>();
+const rootSetting: Required<GridSetting['root']> = {
+ noOverflowStyle: false,
+ rounded: true,
+ outerBorder: true,
+ ...props.settings.root,
+};
+
// non-reactive
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
const rowSetting: Required<GridRowSetting> = {
@@ -1277,32 +1288,48 @@ onMounted(() => {
overflow-x: scroll;
// firefoxだとスクロールバーがセルに重なって見づらくなってしまうのでスペースを空けておく
padding-bottom: 8px;
+
+ &.noOverflowHandling {
+ overflow-x: revert;
+ padding-bottom: 0;
+ }
}
</style>
<style lang="scss">
$borderSetting: solid 0.5px var(--MI_THEME-divider);
-$borderRadius: var(--MI-radius);
// 配下コンポーネントを含めて一括してコントロールするため、scopedもmoduleも使用できない
.mk_grid_border {
+ --rootBorderSetting: none;
+ --borderRadius: 0;
+
border-spacing: 0;
+ &.mk_grid_root_border {
+ --rootBorderSetting: #{$borderSetting};
+ }
+
+ &.mk_grid_root_rounded {
+ --borderRadius: var(--MI-radius);
+ }
+
.mk_grid_thead {
.mk_grid_tr {
.mk_grid_th {
border-left: $borderSetting;
- border-top: $borderSetting;
+ border-top: var(--rootBorderSetting);
&:first-child {
// 左上セル
- border-top-left-radius: $borderRadius;
+ border-left: var(--rootBorderSetting);
+ border-top-left-radius: var(--borderRadius);
}
&:last-child {
// 右上セル
- border-top-right-radius: $borderRadius;
- border-right: $borderSetting;
+ border-top-right-radius: var(--borderRadius);
+ border-right: var(--rootBorderSetting);
}
}
}
@@ -1314,9 +1341,14 @@ $borderRadius: var(--MI-radius);
border-left: $borderSetting;
border-top: $borderSetting;
+ &:first-child {
+ // 左端の列
+ border-left: var(--rootBorderSetting);
+ }
+
&:last-child {
// 一番右端の列
- border-right: $borderSetting;
+ border-right: var(--rootBorderSetting);
}
}
}
@@ -1324,16 +1356,16 @@ $borderRadius: var(--MI-radius);
.last_row {
.mk_grid_td, .mk_grid_th {
// 一番下の行
- border-bottom: $borderSetting;
+ border-bottom: var(--rootBorderSetting);
&:first-child {
// 左下セル
- border-bottom-left-radius: $borderRadius;
+ border-bottom-left-radius: var(--borderRadius);
}
&:last-child {
// 右下セル
- border-bottom-right-radius: $borderRadius;
+ border-bottom-right-radius: var(--borderRadius);
}
}
}
diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue
index 605d27c6d6..aecfe7eaa3 100644
--- a/packages/frontend/src/components/grid/MkHeaderCell.vue
+++ b/packages/frontend/src/components/grid/MkHeaderCell.vue
@@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
:data-grid-cell-col="column.index"
>
<div :class="$style.root">
- <div :class="$style.left"/>
+ <div :class="$style.left"></div>
<div :class="$style.wrapper">
<div ref="contentEl" :class="$style.contentArea">
- <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"/>
+ <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"></span>
<span v-else>{{ text }}</span>
</div>
</div>
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:class="$style.right"
@mousedown="onHandleMouseDown"
@dblclick="onHandleDoubleClick"
- />
+ ></div>
</div>
</div>
</template>
diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts
index 0cb3b6f28b..b82e12b304 100644
--- a/packages/frontend/src/components/grid/grid.ts
+++ b/packages/frontend/src/components/grid/grid.ts
@@ -9,6 +9,11 @@ import { GridColumnSetting } from '@/components/grid/column.js';
import { GridRowSetting } from '@/components/grid/row.js';
export type GridSetting = {
+ root?: {
+ noOverflowStyle?: boolean;
+ rounded?: boolean;
+ outerBorder?: boolean;
+ };
row?: GridRowSetting;
cols: GridColumnSetting[];
cells?: GridCellSetting;
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
index de2b2aca8c..141ab858d3 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts
@@ -22,7 +22,8 @@ export const gridSortOrderKeys = [
'isSensitive',
'localOnly',
'updatedAt',
-];
+] as const satisfies string[];
+
export type GridSortOrderKey = typeof gridSortOrderKeys[number];
export function emptyStrToUndefined(value: string | null) {
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue
new file mode 100644
index 0000000000..4b145db0ed
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue
@@ -0,0 +1,39 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+ ref="uiWindow"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="true"
+ @closed="emit('closed')"
+>
+ <template #header>
+ <i class="ti ti-notes" style="margin-right: 0.5em;"></i> {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}
+ </template>
+ <MkSpacer>
+ <XRegisterLogs :logs="logs"/>
+ </MkSpacer>
+</MkWindow>
+</template>
+
+<script setup lang="ts">
+import MkWindow from '@/components/MkWindow.vue';
+import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
+
+import { i18n } from '@/i18n.js';
+
+import type { RequestLogItem } from './custom-emojis-manager.impl.js';
+
+defineProps<{
+ logs: RequestLogItem[];
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
+</script>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue
new file mode 100644
index 0000000000..ae43507d66
--- /dev/null
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue
@@ -0,0 +1,213 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkWindow
+ ref="uiWindow"
+ :initialWidth="400"
+ :initialHeight="500"
+ :canResize="true"
+ @closed="emit('closed')"
+>
+ <template #header>
+ <i class="ti ti-search" style="margin-right: 0.5em;"></i> {{ i18n.ts.search }}
+ </template>
+ <div :class="$style.root">
+ <MkSpacer>
+ <div class="_gaps">
+ <div class="_gaps_s">
+ <MkInput
+ v-model="model.name"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>name</template>
+ </MkInput>
+ <MkInput
+ v-model="model.category"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>category</template>
+ </MkInput>
+ <MkInput
+ v-model="model.aliases"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>aliases</template>
+ </MkInput>
+
+ <MkInput
+ v-model="model.type"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>type</template>
+ </MkInput>
+ <MkInput
+ v-model="model.license"
+ type="search"
+ autocapitalize="off"
+ >
+ <template #label>license</template>
+ </MkInput>
+ <MkSelect
+ v-model="model.sensitive"
+ >
+ <template #label>sensitive</template>
+ <option :value="null">-</option>
+ <option :value="true">true</option>
+ <option :value="false">false</option>
+ </MkSelect>
+
+ <MkSelect
+ v-model="model.localOnly"
+ >
+ <template #label>localOnly</template>
+ <option :value="null">-</option>
+ <option :value="true">true</option>
+ <option :value="false">false</option>
+ </MkSelect>
+ <MkInput
+ v-model="model.updatedAtFrom"
+ type="date"
+ autocapitalize="off"
+ >
+ <template #label>updatedAt(from)</template>
+ </MkInput>
+ <MkInput
+ v-model="model.updatedAtTo"
+ type="date"
+ autocapitalize="off"
+ >
+ <template #label>updatedAt(to)</template>
+ </MkInput>
+
+ <MkInput
+ v-model="queryRolesText"
+ type="text"
+ readonly
+ autocapitalize="off"
+ @click="onQueryRolesEditClicked"
+ >
+ <template #label>role</template>
+ <template #suffix><i class="ti ti-pencil"></i></template>
+ </MkInput>
+ </div>
+ <MkFolder :spacerMax="8" :spacerMin="8">
+ <template #icon><i class="ti ti-arrows-sort"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
+ <MkSortOrderEditor
+ :baseOrderKeyNames="gridSortOrderKeys"
+ :currentOrders="sortOrders"
+ @update="onSortOrderUpdate"
+ />
+ </MkFolder>
+ </div>
+ </MkSpacer>
+ <div :class="$style.footerActions">
+ <MkButton primary @click="onSearchRequest">
+ {{ i18n.ts.search }}
+ </MkButton>
+ <MkButton @click="onQueryResetButtonClicked">
+ {{ i18n.ts.reset }}
+ </MkButton>
+ </div>
+ </div>
+</MkWindow>
+</template>
+
+<script setup lang="ts">
+import { computed, ref, watch } from 'vue';
+import MkWindow from '@/components/MkWindow.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
+
+import {
+ gridSortOrderKeys,
+} from './custom-emojis-manager.impl.js';
+
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import type { EmojiSearchQuery } from './custom-emojis-manager.local.list.vue';
+import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
+import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
+
+const props = defineProps<{
+ query: EmojiSearchQuery;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+ (ev: 'queryUpdated', query: EmojiSearchQuery): void;
+ (ev: 'sortOrderUpdated', orders: SortOrder<GridSortOrderKey>[]): void;
+ (ev: 'search'): void;
+}>();
+
+const model = ref<EmojiSearchQuery>(props.query);
+const queryRolesText = computed(() => model.value.roles.map(it => it.name).join(','));
+
+watch(model, () => {
+ emit('queryUpdated', model.value);
+}, { deep: true });
+
+const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
+
+function onSortOrderUpdate(orders: SortOrder<GridSortOrderKey>[]) {
+ sortOrders.value = orders;
+ emit('sortOrderUpdated', orders);
+}
+
+function onSearchRequest() {
+ emit('search');
+}
+
+function onQueryResetButtonClicked() {
+ model.value.name = '';
+ model.value.category = '';
+ model.value.aliases = '';
+ model.value.type = '';
+ model.value.license = '';
+ model.value.sensitive = null;
+ model.value.localOnly = null;
+ model.value.updatedAtFrom = '';
+ model.value.updatedAtTo = '';
+ sortOrders.value = [];
+}
+
+async function onQueryRolesEditClicked() {
+ const result = await os.selectRole({
+ initialRoleIds: model.value.roles.map(it => it.id),
+ title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle,
+ publicOnly: true,
+ });
+ if (result.canceled) {
+ return;
+ }
+
+ model.value.roles = result.result;
+}
+</script>
+
+<style module>
+.root {
+ position: relative;
+}
+
+.footerActions {
+ position: sticky;
+ bottom: 0;
+ padding: var(--MI-margin);
+ background-color: var(--MI_THEME-bg);
+ display: flex;
+ gap: 8px;
+ z-index: 1;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
index 55f9632ce4..c4ea3b93e3 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -5,137 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
+ <template #header>
+ <MkPageHeader :overridePageMetadata="headerPageMetadata" :actions="headerActions"/>
+ </template>
<template #default>
- <div class="_gaps">
- <MkFolder>
- <template #icon><i class="ti ti-search"></i></template>
- <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}</template>
- <template #caption>
- {{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }}
- </template>
-
- <div class="_gaps">
- <div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]">
- <MkInput
- v-model="queryName"
- type="search"
- autocapitalize="off"
- :class="[$style.col1, $style.row1]"
- @enter="onSearchRequest"
- >
- <template #label>name</template>
- </MkInput>
- <MkInput
- v-model="queryCategory"
- type="search"
- autocapitalize="off"
- :class="[$style.col2, $style.row1]"
- @enter="onSearchRequest"
- >
- <template #label>category</template>
- </MkInput>
- <MkInput
- v-model="queryAliases"
- type="search"
- autocapitalize="off"
- :class="[$style.col3, $style.row1]"
- @enter="onSearchRequest"
- >
- <template #label>aliases</template>
- </MkInput>
-
- <MkInput
- v-model="queryType"
- type="search"
- autocapitalize="off"
- :class="[$style.col1, $style.row2]"
- @enter="onSearchRequest"
- >
- <template #label>type</template>
- </MkInput>
- <MkInput
- v-model="queryLicense"
- type="search"
- autocapitalize="off"
- :class="[$style.col2, $style.row2]"
- @enter="onSearchRequest"
- >
- <template #label>license</template>
- </MkInput>
- <MkSelect
- v-model="querySensitive"
- :class="[$style.col3, $style.row2]"
- >
- <template #label>sensitive</template>
- <option :value="null">-</option>
- <option :value="true">true</option>
- <option :value="false">false</option>
- </MkSelect>
-
- <MkSelect
- v-model="queryLocalOnly"
- :class="[$style.col1, $style.row3]"
- >
- <template #label>localOnly</template>
- <option :value="null">-</option>
- <option :value="true">true</option>
- <option :value="false">false</option>
- </MkSelect>
- <MkInput
- v-model="queryUpdatedAtFrom"
- type="date"
- autocapitalize="off"
- :class="[$style.col2, $style.row3]"
- @enter="onSearchRequest"
- >
- <template #label>updatedAt(from)</template>
- </MkInput>
- <MkInput
- v-model="queryUpdatedAtTo"
- type="date"
- autocapitalize="off"
- :class="[$style.col3, $style.row3]"
- @enter="onSearchRequest"
- >
- <template #label>updatedAt(to)</template>
- </MkInput>
-
- <MkInput
- v-model="queryRolesText"
- type="text"
- readonly
- autocapitalize="off"
- :class="[$style.col1, $style.row4]"
- @click="onQueryRolesEditClicked"
- >
- <template #label>role</template>
- <template #suffix><span class="ti ti-pencil"/></template>
- </MkInput>
- </div>
-
- <MkFolder :spacerMax="8" :spacerMin="8">
- <template #icon><i class="ti ti-arrows-sort"></i></template>
- <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
- <MkSortOrderEditor
- :baseOrderKeyNames="gridSortOrderKeys"
- :currentOrders="sortOrders"
- @update="onSortOrderUpdate"
- />
- </MkFolder>
-
- <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
- <MkButton primary @click="onSearchRequest">
- {{ i18n.ts.search }}
- </MkButton>
- <MkButton @click="onQueryResetButtonClicked">
- {{ i18n.ts.reset }}
- </MkButton>
- </div>
- </div>
- </MkFolder>
-
- <XRegisterLogsFolder :logs="requestLogs"/>
-
+ <div class="_gaps" :class="$style.main">
<component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/>
<template v-else>
<div v-if="gridItems.length === 0" style="text-align: center">
@@ -143,65 +17,78 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<template v-else>
- <div :class="$style.gridArea">
+ <div :class="$style.grid">
<MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/>
</div>
+ </template>
+ </template>
+ </div>
+ </template>
- <div :class="$style.footer">
- <div :class="$style.left">
- <MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked">
- {{ i18n.ts.delete }} ({{ deleteItemsCount }})
- </MkButton>
- </div>
+ <template #footer>
+ <div v-if="gridItems.length > 0" :class="$style.footer">
+ <div :class="$style.left">
+ <MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked">
+ {{ i18n.ts.delete }} ({{ deleteItemsCount }})
+ </MkButton>
+ </div>
- <div :class="$style.center">
- <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
- </div>
+ <div :class="$style.center">
+ <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/>
+ </div>
- <div :class="$style.right">
- <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked">
- {{ i18n.ts.update }} ({{ updatedItemsCount }})
- </MkButton>
- <MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton>
- </div>
- </div>
- </template>
- </template>
+ <div :class="$style.right">
+ <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked">
+ {{ i18n.ts.update }} ({{ updatedItemsCount }})
+ </MkButton>
+ <MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton>
+ </div>
</div>
</template>
</MkStickyContainer>
</template>
+<script lang="ts">
+import type { SortOrder } from '@/components/MkSortOrderEditor.define.js';
+import type { GridSortOrderKey } from './custom-emojis-manager.impl.js';
+
+export type EmojiSearchQuery = {
+ name: string | null;
+ category: string | null;
+ aliases: string | null;
+ type: string | null;
+ license: string | null;
+ updatedAtFrom: string | null;
+ updatedAtTo: string | null;
+ sensitive: string | null;
+ localOnly: string | null;
+ roles: { id: string, name: string }[];
+ sortOrders: SortOrder<GridSortOrderKey>[];
+ limit: number;
+};
+</script>
+
<script setup lang="ts">
-import { computed, onMounted, ref, useCssModule } from 'vue';
+import { computed, defineAsyncComponent, onMounted, ref, nextTick, useCssModule } from 'vue';
import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
import {
emptyStrToEmptyArray,
emptyStrToNull,
emptyStrToUndefined,
- GridSortOrderKey,
- gridSortOrderKeys,
RequestLogItem,
roleIdsParser,
} from '@/pages/admin/custom-emojis-manager.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import { i18n } from '@/i18n.js';
-import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { validators } from '@/components/grid/cell-validators.js';
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import MkPagingButtons from '@/components/MkPagingButtons.vue';
-import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import MkSelect from '@/components/MkSelect.vue';
-import { deviceKind } from '@/scripts/device-kind.js';
import { GridSetting } from '@/components/grid/grid.js';
import { selectFile } from '@/scripts/select-file.js';
import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
-import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
-import { SortOrder } from '@/components/MkSortOrderEditor.define.js';
import { useLoading } from "@/components/hook/useLoading.js";
type GridItem = {
@@ -230,6 +117,11 @@ function setupGrid(): GridSetting {
const regex = validators.regex(/^[a-zA-Z0-9_]+$/);
const unique = validators.unique();
return {
+ root: {
+ noOverflowStyle: true,
+ rounded: false,
+ outerBorder: false,
+ },
row: {
showNumber: true,
selectable: true,
@@ -381,16 +273,22 @@ const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]);
const allPages = ref<number>(0);
const currentPage = ref<number>(0);
-const queryName = ref<string | null>(null);
-const queryCategory = ref<string | null>(null);
-const queryAliases = ref<string | null>(null);
-const queryType = ref<string | null>(null);
-const queryLicense = ref<string | null>(null);
-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 searchQuery = ref<EmojiSearchQuery>({
+ name: null,
+ category: null,
+ aliases: null,
+ type: null,
+ license: null,
+ updatedAtFrom: null,
+ updatedAtTo: null,
+ sensitive: null,
+ localOnly: null,
+ roles: [],
+ sortOrders: [],
+ limit: 25,
+});
+let searchWindowOpening = false;
+
const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
const requestLogs = ref<RequestLogItem[]>([]);
@@ -399,8 +297,6 @@ const gridItems = ref<GridItem[]>([]);
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(','));
const updatedItemsCount = computed(() => {
return gridItems.value.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(originGridItems.value[idx])).length;
});
@@ -422,12 +318,11 @@ async function onUpdateButtonClicked() {
return;
}
- const confirm = await os.confirm({
+ const { canceled } = await os.confirm({
type: 'info',
- title: i18n.ts._customEmojisManager._local._list.confirmUpdateEmojisTitle,
text: i18n.tsx._customEmojisManager._local._list.confirmUpdateEmojisDescription({ count: updatedItems.length }),
});
- if (confirm.canceled) {
+ if (canceled) {
return;
}
@@ -458,7 +353,7 @@ async function onUpdateButtonClicked() {
if (failedItems.length > 0) {
await os.alert({
type: 'error',
- title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle,
+ title: i18n.ts.somethingHappened,
text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
});
}
@@ -489,12 +384,11 @@ async function onDeleteButtonClicked() {
return;
}
- const confirm = await os.confirm({
+ const { canceled } = await os.confirm({
type: 'info',
- title: i18n.ts._customEmojisManager._local._list.confirmDeleteEmojisTitle,
text: i18n.tsx._customEmojisManager._local._list.confirmDeleteEmojisDescription({ count: deleteItems.length }),
});
- if (confirm.canceled) {
+ if (canceled) {
return;
}
@@ -508,47 +402,35 @@ async function onDeleteButtonClicked() {
);
}
-function onGridResetButtonClicked() {
- refreshGridItems();
-}
-
-async function onQueryRolesEditClicked() {
- const result = await os.selectRole({
- initialRoleIds: queryRoles.value.map(it => it.id),
- title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle,
- publicOnly: true,
+async function onGridResetButtonClicked() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts.resetAreYouSure,
+ text: i18n.ts._customEmojisManager._local._list.confirmResetDescription,
});
- if (result.canceled) {
- return;
- }
- queryRoles.value = result.result;
-}
+ if (canceled) return;
-function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) {
- sortOrders.value = _sortOrders;
+ refreshGridItems();
}
async function onSearchRequest() {
await refreshCustomEmojis();
}
-function onQueryResetButtonClicked() {
- queryName.value = null;
- queryCategory.value = null;
- queryAliases.value = null;
- queryType.value = null;
- queryLicense.value = null;
- queryUpdatedAtFrom.value = null;
- queryUpdatedAtTo.value = null;
- querySensitive.value = null;
- queryLocalOnly.value = null;
- queryRoles.value = [];
-}
-
async function onPageChanged(pageNumber: number) {
+ if (updatedItemsCount.value > 0) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts._customEmojisManager._local._list.confirmMovePage,
+ text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption,
+ });
+ if (canceled) return;
+ }
+
currentPage.value = pageNumber;
- await refreshCustomEmojis();
+ await nextTick();
+ refreshCustomEmojis();
}
function onGridEvent(event: GridEvent) {
@@ -574,19 +456,19 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) {
}
async function refreshCustomEmojis() {
- const limit = 100;
+ const limit = searchQuery.value.limit;
const query: Misskey.entities.V2AdminEmojiListRequest['query'] = {
- name: emptyStrToUndefined(queryName.value),
- type: emptyStrToUndefined(queryType.value),
- aliases: emptyStrToUndefined(queryAliases.value),
- category: emptyStrToUndefined(queryCategory.value),
- license: emptyStrToUndefined(queryLicense.value),
- isSensitive: querySensitive.value ? Boolean(querySensitive.value).valueOf() : undefined,
- localOnly: queryLocalOnly.value ? Boolean(queryLocalOnly.value).valueOf() : undefined,
- updatedAtFrom: emptyStrToUndefined(queryUpdatedAtFrom.value),
- updatedAtTo: emptyStrToUndefined(queryUpdatedAtTo.value),
- roleIds: queryRoles.value.map(it => it.id),
+ name: emptyStrToUndefined(searchQuery.value.name),
+ type: emptyStrToUndefined(searchQuery.value.type),
+ aliases: emptyStrToUndefined(searchQuery.value.aliases),
+ category: emptyStrToUndefined(searchQuery.value.category),
+ license: emptyStrToUndefined(searchQuery.value.license),
+ isSensitive: searchQuery.value.sensitive ? Boolean(searchQuery.value.sensitive).valueOf() : undefined,
+ localOnly: searchQuery.value.localOnly ? Boolean(searchQuery.value.localOnly).valueOf() : undefined,
+ updatedAtFrom: emptyStrToUndefined(searchQuery.value.updatedAtFrom),
+ updatedAtTo: emptyStrToUndefined(searchQuery.value.updatedAtTo),
+ roleIds: searchQuery.value.roles.map(it => it.id),
hostType: 'local',
};
@@ -635,6 +517,83 @@ onMounted(async () => {
await refreshCustomEmojis();
});
+const headerPageMetadata = computed(() => ({
+ title: i18n.ts._customEmojisManager._local.tabTitleList,
+ icon: 'ti ti-icons',
+}));
+
+const headerActions = computed(() => [{
+ icon: 'ti ti-search',
+ text: i18n.ts.search,
+ handler: () => {
+ if (searchWindowOpening) return;
+ searchWindowOpening = true;
+ const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.search.vue')), {
+ query: searchQuery.value,
+ }, {
+ queryUpdated: (query: EmojiSearchQuery) => {
+ searchQuery.value = query;
+ },
+ sortOrderUpdated: (orders: SortOrder<GridSortOrderKey>[]) => {
+ sortOrders.value = orders;
+ },
+ search: () => {
+ onSearchRequest();
+ },
+ closed: () => {
+ dispose();
+ searchWindowOpening = false;
+ },
+ });
+ },
+}, {
+ icon: 'ti ti-list-numbers',
+ text: i18n.ts._customEmojisManager._gridCommon.searchLimit,
+ handler: (ev: MouseEvent) => {
+ async function changeSearchLimit(to: number) {
+ if (updatedItemsCount.value > 0) {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ title: i18n.ts._customEmojisManager._local._list.confirmChangeView,
+ text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption,
+ });
+ if (canceled) return;
+ }
+
+ searchQuery.value.limit = to;
+ refreshCustomEmojis();
+ }
+
+ os.popupMenu([{
+ type: 'radioOption',
+ text: '25',
+ active: computed(() => searchQuery.value.limit === 25),
+ action: () => changeSearchLimit(25),
+ }, {
+ type: 'radioOption',
+ text: '50',
+ active: computed(() => searchQuery.value.limit === 50),
+ action: () => changeSearchLimit(50),
+ }, {
+ type: 'radioOption',
+ text: '100',
+ active: computed(() => searchQuery.value.limit === 100),
+ action: () => changeSearchLimit(100),
+ }], ev.currentTarget ?? ev.target);
+ },
+}, {
+ icon: 'ti ti-notes',
+ text: i18n.ts._customEmojisManager._gridCommon.registrationLogs,
+ handler: () => {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.logs.vue')), {
+ logs: requestLogs.value,
+ }, {
+ closed: () => {
+ dispose();
+ },
+ });
+ }
+}]);
</script>
<style module lang="scss">
@@ -650,77 +609,21 @@ onMounted(async () => {
background-color: var(--MI_THEME-infoBg);
}
-.row1 {
- grid-row: 1 / 2;
-}
-
-.row2 {
- grid-row: 2 / 3;
-}
-
-.row3 {
- grid-row: 3 / 4;
-}
-
-.row4 {
- grid-row: 4 / 5;
-}
-
-.col1 {
- grid-column: 1 / 2;
-}
-
-.col2 {
- grid-column: 2 / 3;
-}
-
-.col3 {
- grid-column: 3 / 4;
-}
-
-.searchArea {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 16px;
-}
-
-.searchAreaSp {
- display: flex;
- flex-direction: column;
- gap: 8px;
-}
-
-.searchButtons {
- display: flex;
- justify-content: flex-end;
- align-items: flex-end;
- gap: 8px;
-}
-
-.searchButtonsSp {
- display: flex;
- justify-content: center;
- align-items: center;
- gap: 8px;
+.main {
+ height: calc(100vh - var(--MI-stickyTop) - var(--MI-stickyBottom));
+ overflow: scroll;
}
-.gridArea {
- padding-top: 8px;
- padding-bottom: 8px;
+.grid {
+ width: max-content;
+ border-bottom: 1px solid var(--MI_THEME-divider);
}
.footer {
background-color: var(--MI_THEME-bg);
- position: sticky;
- left:0;
- bottom:0;
- z-index: 1;
- // stickyで追従させる都合上、フッター自身でpaddingを持つ必要があるため、親要素で画一的に指定している分をネガティブマージンで相殺している
- margin-top: calc(var(--MI-margin) * -1);
- margin-bottom: calc(var(--MI-margin) * -1);
- padding-top: var(--MI-margin);
- padding-bottom: var(--MI-margin);
+ padding: var(--MI-margin);
+ border-top: 1px solid var(--MI_THEME-divider);
display: grid;
grid-template-columns: 1fr 1fr 1fr;
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
index a3de5de569..cc8b625cd5 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
@@ -30,7 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
- <XRegisterLogsFolder :logs="requestLogs"/>
+ <MkFolder>
+ <template #icon><i class="ti ti-notes"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
+ <template #caption>
+ {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
+ </template>
+ <XRegisterLogs :logs="requestLogs"/>
+ </MkFolder>
<div
:class="[$style.uploadBox, [isDragOver ? $style.dragOver : {}]]"
@@ -91,7 +98,7 @@ import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js'
import { uploadFile } from '@/scripts/upload.js';
import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
-import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
+import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import { GridSetting } from '@/components/grid/grid.js';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
import { GridRow } from '@/components/grid/row.js';
@@ -245,7 +252,6 @@ const isDragOver = ref<boolean>(false);
async function onRegistryClicked() {
const dialogSelection = await os.confirm({
type: 'info',
- title: i18n.ts._customEmojisManager._local._register.confirmRegisterEmojisTitle,
text: i18n.tsx._customEmojisManager._local._register.confirmRegisterEmojisDescription({ count: MAXIMUM_EMOJI_REGISTER_COUNT }),
});
@@ -279,7 +285,7 @@ async function onRegistryClicked() {
if (failedItems.length > 0) {
await os.alert({
type: 'error',
- title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle,
+ title: i18n.ts.somethingHappened,
text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
});
}
@@ -299,7 +305,6 @@ async function onRegistryClicked() {
async function onClearClicked() {
const result = await os.confirm({
type: 'warning',
- title: i18n.ts._customEmojisManager._local._register.confirmClearEmojisTitle,
text: i18n.ts._customEmojisManager._local._register.confirmClearEmojisDescription,
});
@@ -314,7 +319,6 @@ async function onDrop(ev: DragEvent) {
const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it));
const confirm = await os.confirm({
type: 'info',
- title: i18n.ts._customEmojisManager._local._register.confirmUploadEmojisTitle,
text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }),
});
if (confirm.canceled) {
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
index ea4303f342..6e7e7e53e3 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue
@@ -4,33 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<div class="_gaps" :class="$style.root">
- <MkTab v-model="modeTab" style="margin-bottom: var(--margin);">
- <option value="list">{{ i18n.ts._customEmojisManager._local.tabTitleList }}</option>
- <option value="register">{{ i18n.ts._customEmojisManager._local.tabTitleRegister }}</option>
- </MkTab>
-
- <div>
- <XListComponent v-if="modeTab === 'list'"/>
- <XRegisterComponent v-else/>
- </div>
-</div>
+<MkStickyContainer>
+ <template #header>
+ <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs" hideTitle thin/>
+ </template>
+ <XListComponent v-if="headerTab === 'list'" key="localList"/>
+ <MkSpacer v-else key="localRegister">
+ <XRegisterComponent/>
+ </MkSpacer>
+</MkStickyContainer>
</template>
<script setup lang="ts">
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
-import MkTab from '@/components/MkTab.vue';
import XListComponent from '@/pages/admin/custom-emojis-manager.local.list.vue';
import XRegisterComponent from '@/pages/admin/custom-emojis-manager.local.register.vue';
type PageMode = 'list' | 'register';
-const modeTab = ref<PageMode>('list');
-</script>
+const headerTab = ref<PageMode>('list');
-<style module lang="scss">
-.root {
- padding: var(--MI-margin);
-}
-</style>
+const headerTabs = computed(() => [{
+ key: 'list',
+ title: i18n.ts._customEmojisManager._local.tabTitleList,
+}, {
+ key: 'register',
+ title: i18n.ts._customEmojisManager._local.tabTitleRegister,
+}]);
+</script>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue
index f75f6c0da5..eef55a9f7e 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue
@@ -4,47 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkFolder>
- <template #icon><i class="ti ti-notes"></i></template>
- <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
- <template #caption>
- {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
- </template>
-
- <div>
- <div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;">
- <MkSwitch v-model="showingSuccessLogs">
- <template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template>
- </MkSwitch>
- <div>
- <div v-if="filteredLogs.length > 0">
- <MkGrid
- :data="filteredLogs"
- :settings="setupGrid()"
- />
- </div>
- <div v-else>
- {{ i18n.ts._customEmojisManager._logs.failureLogNothing }}
- </div>
+<div>
+ <div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;">
+ <MkSwitch v-model="showingSuccessLogs">
+ <template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template>
+ </MkSwitch>
+ <div>
+ <div v-if="filteredLogs.length > 0">
+ <MkGrid
+ :data="filteredLogs"
+ :settings="setupGrid()"
+ />
+ </div>
+ <div v-else>
+ {{ i18n.ts._customEmojisManager._logs.failureLogNothing }}
</div>
- </div>
- <div v-else>
- {{ i18n.ts._customEmojisManager._logs.logNothing }}
</div>
</div>
-</MkFolder>
+ <div v-else>
+ {{ i18n.ts._customEmojisManager._logs.logNothing }}
+ </div>
+</div>
</template>
<script setup lang="ts">
-
import { computed, ref, toRefs } from 'vue';
import { i18n } from '@/i18n.js';
-import { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
import MkGrid from '@/components/grid/MkGrid.vue';
import MkSwitch from '@/components/MkSwitch.vue';
-import { GridSetting } from '@/components/grid/grid.js';
import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
-import MkFolder from '@/components/MkFolder.vue';
+
+import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
+import type { GridSetting } from '@/components/grid/grid.js';
function setupGrid(): GridSetting {
return {
@@ -94,9 +85,4 @@ const filteredLogs = computed(() => {
const forceShowing = showingSuccessLogs.value;
return logs.value.filter((log) => forceShowing || log.failed);
});
-
</script>
-
-<style module lang="scss">
-
-</style>
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
index 14a3b71e53..eecf8d7390 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</div>
+ <hr>
+
<MkFolder :spacerMax="8" :spacerMin="8">
<template #icon><i class="ti ti-arrows-sort"></i></template>
<template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template>
@@ -74,6 +76,14 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
</MkFolder>
+ <MkInput
+ v-model="queryLimit"
+ type="number"
+ :max="100"
+ >
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchLimit }}</template>
+ </MkInput>
+
<div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]">
<MkButton primary @click="onSearchRequest">
{{ i18n.ts.search }}
@@ -85,7 +95,14 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkFolder>
- <XRegisterLogsFolder :logs="requestLogs"/>
+ <MkFolder>
+ <template #icon><i class="ti ti-notes"></i></template>
+ <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template>
+ <template #caption>
+ {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }}
+ </template>
+ <XRegisterLogs :logs="requestLogs"/>
+ </MkFolder>
<component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/>
<template v-else>
@@ -139,7 +156,7 @@ import {
} from '@/pages/admin/custom-emojis-manager.impl.js';
import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
import MkFolder from '@/components/MkFolder.vue';
-import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue';
+import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
import * as os from '@/os.js';
import { GridSetting } from '@/components/grid/grid.js';
import { deviceKind } from '@/scripts/device-kind.js';
@@ -246,6 +263,7 @@ const queryHost = ref<string | null>(null);
const queryLicense = ref<string | null>(null);
const queryUri = ref<string | null>(null);
const queryPublicUrl = ref<string | null>(null);
+const queryLimit = ref<number>(25);
const previousQuery = ref<string | undefined>(undefined);
const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]);
const requestLogs = ref<RequestLogItem[]>([]);
@@ -325,7 +343,7 @@ async function importEmojis(targets: GridItem[]) {
if (failedItems.length > 0) {
await os.alert({
type: 'error',
- title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle,
+ title: i18n.ts.somethingHappened,
text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription,
});
}
@@ -355,7 +373,7 @@ async function refreshCustomEmojis() {
}
const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', {
- limit: 100,
+ limit: queryLimit.value,
query: query,
page: currentPage.value,
sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`) as never[],
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
index a952a5a3d1..fb930064ff 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
@@ -5,12 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div>
- <!-- コンテナが入れ子になるのでz-indexが被らないよう大きめの数値を設定する-->
- <MkStickyContainer :headerZIndex="2000">
+ <MkStickyContainer>
<template #header>
<MkPageHeader v-model:tab="headerTab" :tabs="headerTabs"/>
</template>
- <XGridLocalComponent v-if="headerTab === 'local'"/>
+ <XGridLocalComponent v-if="headerTab === 'local'" :class="$style.local"/>
<XGridRemoteComponent v-else/>
</MkStickyContainer>
</div>
@@ -40,5 +39,13 @@ const headerTabs = computed(() => [{
definePageMetadata(computed(() => ({
title: i18n.ts.customEmojis,
icon: 'ti ti-icons',
+ needWideArea: true,
})));
</script>
+
+<style lang="css" module>
+.local {
+ height: calc(100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom));
+ overflow: clip;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 969ca8b9e8..ea5fa457f2 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue';
import { i18n } from '@/i18n.js';
import MkSuperMenu from '@/components/MkSuperMenu.vue';
+import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
import MkInfo from '@/components/MkInfo.vue';
import { instance } from '@/instance.js';
import { lookup } from '@/scripts/lookup.js';
@@ -55,7 +56,7 @@ const indexInfo = {
provide('shouldOmitHeaderTitle', false);
-const INFO = ref(indexInfo);
+const INFO = ref<PageMetadata>(indexInfo);
const childInfo = ref<null | PageMetadata>(null);
const narrow = ref(false);
const view = ref(null);
@@ -81,7 +82,7 @@ const ro = new ResizeObserver((entries, observer) => {
narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
});
-const menuDef = computed(() => [{
+const menuDef = computed<SuperMenuDef[]>(() => [{
title: i18n.ts.quickAction,
items: [{
type: 'button',
@@ -89,7 +90,7 @@ const menuDef = computed(() => [{
text: i18n.ts.lookup,
action: adminLookup,
}, ...(instance.disableRegistration ? [{
- type: 'button',
+ type: 'button' as const,
icon: 'ti ti-user-plus',
text: i18n.ts.createInviteCode,
action: invite,
@@ -333,12 +334,14 @@ defineExpose({
height: 100%;
> .nav {
+ position: sticky;
+ top: 0;
width: 32%;
max-width: 280px;
box-sizing: border-box;
border-right: solid 0.5px var(--MI_THEME-divider);
overflow: auto;
- height: 100%;
+ height: 100dvh;
}
> .main {
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 96a95f1635..b5a6d719d1 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -44,7 +44,7 @@ const indexInfo = {
icon: 'ti ti-settings',
hideHeader: true,
};
-const INFO = ref(indexInfo);
+const INFO = ref<PageMetadata>(indexInfo);
const el = shallowRef<HTMLElement | null>(null);
const childInfo = ref<null | PageMetadata>(null);
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index d739c2e1cd..94998b7be6 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWidgets/>
</div>
- <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
+ <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>