summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authorsyuilo <4439005+syuilo@users.noreply.github.com>2025-05-06 14:41:31 +0900
committerGitHub <noreply@github.com>2025-05-06 14:41:31 +0900
commit8959bfa1c0b558888aa7da207f8166092c51a353 (patch)
tree6724fbcd6be6831878d4dfa6bbddcffc6052b81d /packages/frontend/src/components
parentchore(deps): sharpを固定 (#15957) (diff)
downloadmisskey-8959bfa1c0b558888aa7da207f8166092c51a353.tar.gz
misskey-8959bfa1c0b558888aa7da207f8166092c51a353.tar.bz2
misskey-8959bfa1c0b558888aa7da207f8166092c51a353.zip
refactor(frontend): 空/エラー結果表示をコンポーネント化 (#15963)
* wip * wip * wip * wip * wip * Update MkResult.vue * Add storybook story for MkResult (#15964) * Update MkResult.vue --------- Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/MkChannelList.vue8
-rw-r--r--packages/frontend/src/components/MkChatHistories.vue4
-rw-r--r--packages/frontend/src/components/MkFormDialog.vue6
-rw-r--r--packages/frontend/src/components/MkNotes.vue8
-rw-r--r--packages/frontend/src/components/MkNotification.vue2
-rw-r--r--packages/frontend/src/components/MkNotifications.vue8
-rw-r--r--packages/frontend/src/components/MkPagination.vue8
-rw-r--r--packages/frontend/src/components/MkTimeline.vue8
-rw-r--r--packages/frontend/src/components/MkUserList.vue8
-rw-r--r--packages/frontend/src/components/global/MkError.vue4
-rw-r--r--packages/frontend/src/components/global/MkResult.stories.impl.ts57
-rw-r--r--packages/frontend/src/components/global/MkResult.vue44
-rw-r--r--packages/frontend/src/components/index.ts3
13 files changed, 114 insertions, 54 deletions
diff --git a/packages/frontend/src/components/MkChannelList.vue b/packages/frontend/src/components/MkChannelList.vue
index fdb7d2a1c4..d0b50f04f2 100644
--- a/packages/frontend/src/components/MkChannelList.vue
+++ b/packages/frontend/src/components/MkChannelList.vue
@@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkPagination :pagination="pagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.notFound }}</div>
- </div>
- </template>
+ <template #empty><MkResult type="empty"/></template>
<template #default="{ items }">
<MkChannelPreview v-for="item in items" :key="item.id" class="_margin" :channel="extractor(item)"/>
@@ -23,7 +18,6 @@ import type { Paging } from '@/components/MkPagination.vue';
import MkChannelPreview from '@/components/MkChannelPreview.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{
pagination: Paging;
diff --git a/packages/frontend/src/components/MkChatHistories.vue b/packages/frontend/src/components/MkChatHistories.vue
index c508ea8451..b33ed428c7 100644
--- a/packages/frontend/src/components/MkChatHistories.vue
+++ b/packages/frontend/src/components/MkChatHistories.vue
@@ -28,9 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</MkA>
</div>
-<div v-if="!initializing && history.length == 0" class="_fullinfo">
- <div>{{ i18n.ts._chat.noHistory }}</div>
-</div>
+<MkResult v-if="!initializing && history.length == 0" type="empty" :text="i18n.ts._chat.noHistory"/>
<MkLoading v-if="initializing"/>
</template>
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 0884cdc016..6ac4441cac 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -62,10 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
</template>
</div>
- <div v-else class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.nothing }}</div>
- </div>
+ <MkResult v-else type="empty"/>
</div>
</MkModalWindow>
</template>
@@ -83,7 +80,6 @@ import XFile from './MkFormDialog.file.vue';
import type { Form } from '@/utility/form.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
const props = defineProps<{
title: string;
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 9d862a4eac..509099e0b9 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkPagination ref="pagingComponent" :pagination="pagination" :disableAutoLoad="disableAutoLoad">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.noNotes }}</div>
- </div>
- </template>
+ <template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
<template #default="{ items: notes }">
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]">
@@ -34,7 +29,6 @@ import type { Paging } from '@/components/MkPagination.vue';
import MkNote from '@/components/MkNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
const props = defineProps<{
pagination: Paging;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 9672efca0a..21104b41df 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -11,7 +11,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ti ti-heart" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
- <img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div
@@ -176,7 +175,6 @@ import { userPage } from '@/filters/user.js';
import { i18n } from '@/i18n.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { ensureSignin } from '@/i.js';
-import { infoImageUrl } from '@/instance.js';
const $i = ensureSignin();
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 308a077bd9..3c88b8af0d 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -6,12 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reload()">
<MkPagination ref="pagingComponent" :pagination="pagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.noNotifications }}</div>
- </div>
- </template>
+ <template #empty><MkResult type="empty" :text="i18n.ts.noNotifications"/></template>
<template #default="{ items: notifications }">
<component
@@ -42,7 +37,6 @@ import XNotification from '@/components/MkNotification.vue';
import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';
import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index 9adc3d98da..54da5a889d 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -16,12 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkError v-else-if="error" @retry="init()"/>
<div v-else-if="empty" key="_empty_">
- <slot name="empty">
- <div class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.nothing }}</div>
- </div>
- </slot>
+ <slot name="empty"><MkResult type="empty"/></slot>
</div>
<div v-else ref="rootEl" class="_gaps">
@@ -88,7 +83,6 @@ function concatMapWithArray(map: MisskeyEntityMap, entities: MisskeyEntity[]): M
</script>
<script lang="ts" setup>
-import { infoImageUrl } from '@/instance.js';
import MkButton from '@/components/MkButton.vue';
const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index e2c261787b..6a265aa836 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -6,12 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="prefer.s.enablePullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => reloadTimeline()">
<MkPagination v-if="paginationQuery" ref="pagingComponent" :pagination="paginationQuery" @queue="emit('queue', $event)">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.noNotes }}</div>
- </div>
- </template>
+ <template #empty><MkResult type="empty" :text="i18n.ts.noNotes"/></template>
<template #default="{ items: notes }">
<component
@@ -53,7 +48,6 @@ import { prefer } from '@/preferences.js';
import MkNote from '@/components/MkNote.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{
src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index 0d1ffd715f..90087cb000 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -5,12 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkPagination :pagination="pagination">
- <template #empty>
- <div class="_fullinfo">
- <img :src="infoImageUrl" draggable="false"/>
- <div>{{ i18n.ts.noUsers }}</div>
- </div>
- </template>
+ <template #empty><MkResult type="empty" :text="i18n.ts.noUsers"/></template>
<template #default="{ items }">
<div :class="$style.root">
@@ -25,7 +20,6 @@ import type { Paging } from '@/components/MkPagination.vue';
import MkUserInfo from '@/components/MkUserInfo.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
-import { infoImageUrl } from '@/instance.js';
const props = withDefaults(defineProps<{
pagination: Paging;
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index 95ed255189..bc3a282e40 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
<div :class="$style.root">
- <img :class="$style.img" :src="serverErrorImageUrl" draggable="false"/>
+ <img v-if="instance.serverErrorImageUrl" :class="$style.img" :src="instance.serverErrorImageUrl" draggable="false"/>
<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
<MkButton :class="$style.button" @click="() => emit('retry')">{{ i18n.ts.retry }}</MkButton>
</div>
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
-import { serverErrorImageUrl } from '@/instance.js';
+import { instance } from '@/instance.js';
const emit = defineEmits<{
(ev: 'retry'): void;
diff --git a/packages/frontend/src/components/global/MkResult.stories.impl.ts b/packages/frontend/src/components/global/MkResult.stories.impl.ts
new file mode 100644
index 0000000000..05f8c9069b
--- /dev/null
+++ b/packages/frontend/src/components/global/MkResult.stories.impl.ts
@@ -0,0 +1,57 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import MkResult from './MkResult.vue';
+import type { StoryObj } from '@storybook/vue3';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkResult,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkResult v-bind="props" />',
+ };
+ },
+ args: {
+ type: 'empty',
+ text: 'Lorem Ipsum',
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkResult>;
+export const emptyWithNoText = {
+ ...Default,
+ args: {
+ ...Default.args,
+ text: undefined,
+ },
+} satisfies StoryObj<typeof MkResult>;
+export const notFound = {
+ ...Default,
+ args: {
+ ...Default.args,
+ type: 'notFound',
+ },
+} satisfies StoryObj<typeof MkResult>;
+export const errorType = {
+ ...Default,
+ args: {
+ ...Default.args,
+ type: 'error',
+ },
+} satisfies StoryObj<typeof MkResult>;
diff --git a/packages/frontend/src/components/global/MkResult.vue b/packages/frontend/src/components/global/MkResult.vue
new file mode 100644
index 0000000000..51cf8a860a
--- /dev/null
+++ b/packages/frontend/src/components/global/MkResult.vue
@@ -0,0 +1,44 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="[$style.root]" class="_gaps">
+ <img v-if="type === 'empty' && instance.infoImageUrl" :src="instance.infoImageUrl" draggable="false" :class="$style.img"/>
+ <i v-else-if="type === 'empty'" class="ti ti-info-circle" :class="$style.icon"></i>
+ <div>{{ props.text ?? (type === 'empty' ? i18n.ts.nothing : type === 'notFound' ? i18n.ts.notFound : null) }}</div>
+ <slot></slot>
+</div>
+</template>
+
+<script lang="ts" setup>
+import {} from 'vue';
+import { instance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+
+const props = defineProps<{
+ type: 'empty' | 'notFound' | 'error';
+ text?: string;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+ position: relative;
+ text-align: center;
+ padding: 32px;
+}
+
+.img {
+ vertical-align: bottom;
+ height: 128px;
+ margin-bottom: 16px;
+ border-radius: 16px;
+}
+
+.icon {
+ font-size: 24px;
+ margin: 0 auto;
+}
+</style>
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index ec6ea7c569..33d3532c1d 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -24,6 +24,7 @@ import MkAd from './global/MkAd.vue';
import MkPageHeader from './global/MkPageHeader.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
import MkLazy from './global/MkLazy.vue';
+import MkResult from './global/MkResult.vue';
import PageWithHeader from './global/PageWithHeader.vue';
import PageWithAnimBg from './global/PageWithAnimBg.vue';
import SearchMarker from './global/SearchMarker.vue';
@@ -61,6 +62,7 @@ export const components = {
MkPageHeader: MkPageHeader,
MkStickyContainer: MkStickyContainer,
MkLazy: MkLazy,
+ MkResult: MkResult,
PageWithHeader: PageWithHeader,
PageWithAnimBg: PageWithAnimBg,
SearchMarker: SearchMarker,
@@ -92,6 +94,7 @@ declare module '@vue/runtime-core' {
MkPageHeader: typeof MkPageHeader;
MkStickyContainer: typeof MkStickyContainer;
MkLazy: typeof MkLazy;
+ MkResult: typeof MkResult;
PageWithHeader: typeof PageWithHeader;
PageWithAnimBg: typeof PageWithAnimBg;
SearchMarker: typeof SearchMarker;