enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように (#14286)
* enhance: 管理画面でアーカイブにしたお知らせを表示できるように * Update Changelog
This commit is contained in:
		
							parent
							
								
									b44313fe3c
								
							
						
					
					
						commit
						de3ddb5b44
					
				
					 7 changed files with 127 additions and 63 deletions
				
			
		| 
						 | 
					@ -9,6 +9,7 @@
 | 
				
			||||||
- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
 | 
					- Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に
 | 
				
			||||||
  - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
 | 
					  - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます
 | 
				
			||||||
- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
 | 
					- Feat: ユーザ作成時にSystemWebhookを送信可能に #14281
 | 
				
			||||||
 | 
					- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように
 | 
				
			||||||
- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
 | 
					- Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正
 | 
				
			||||||
- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
 | 
					- Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題
 | 
				
			||||||
- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
 | 
					- Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								locales/index.d.ts
									
										
									
									
										vendored
									
									
								
							| 
						 | 
					@ -4440,6 +4440,14 @@ export interface Locale extends ILocale {
 | 
				
			||||||
     * アーカイブ
 | 
					     * アーカイブ
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    "archive": string;
 | 
					    "archive": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * アーカイブ済み
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "archived": string;
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * アーカイブ解除
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    "unarchive": string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * {name}をアーカイブしますか?
 | 
					     * {name}をアーカイブしますか?
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1106,6 +1106,8 @@ preservedUsernames: "予約ユーザー名"
 | 
				
			||||||
preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
 | 
					preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。"
 | 
				
			||||||
createNoteFromTheFile: "このファイルからノートを作成"
 | 
					createNoteFromTheFile: "このファイルからノートを作成"
 | 
				
			||||||
archive: "アーカイブ"
 | 
					archive: "アーカイブ"
 | 
				
			||||||
 | 
					archived: "アーカイブ済み"
 | 
				
			||||||
 | 
					unarchive: "アーカイブ解除"
 | 
				
			||||||
channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
 | 
					channelArchiveConfirmTitle: "{name}をアーカイブしますか?"
 | 
				
			||||||
channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
 | 
					channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。"
 | 
				
			||||||
thisChannelArchived: "このチャンネルはアーカイブされています。"
 | 
					thisChannelArchived: "このチャンネルはアーカイブされています。"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -69,6 +69,7 @@ export const paramDef = {
 | 
				
			||||||
		sinceId: { type: 'string', format: 'misskey:id' },
 | 
							sinceId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
		untilId: { type: 'string', format: 'misskey:id' },
 | 
							untilId: { type: 'string', format: 'misskey:id' },
 | 
				
			||||||
		userId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
							userId: { type: 'string', format: 'misskey:id', nullable: true },
 | 
				
			||||||
 | 
							status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	required: [],
 | 
						required: [],
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
| 
						 | 
					@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
 | 
				
			||||||
	) {
 | 
						) {
 | 
				
			||||||
		super(meta, paramDef, async (ps, me) => {
 | 
							super(meta, paramDef, async (ps, me) => {
 | 
				
			||||||
			const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
 | 
								const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (ps.status === 'archived') {
 | 
				
			||||||
 | 
									query.andWhere('announcement.isActive = false');
 | 
				
			||||||
 | 
								} else if (ps.status === 'active') {
 | 
				
			||||||
				query.andWhere('announcement.isActive = true');
 | 
									query.andWhere('announcement.isActive = true');
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (ps.userId) {
 | 
								if (ps.userId) {
 | 
				
			||||||
				query.andWhere('announcement.userId = :userId', { userId: ps.userId });
 | 
									query.andWhere('announcement.userId = :userId', { userId: ps.userId });
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
	<div class="buttons right">
 | 
						<div class="buttons right">
 | 
				
			||||||
		<template v-if="actions">
 | 
							<template v-if="actions">
 | 
				
			||||||
			<template v-for="action in actions">
 | 
								<template v-for="action in actions">
 | 
				
			||||||
				<MkButton v-if="action.asFullButton" class="fullButton" primary @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
 | 
									<MkButton v-if="action.asFullButton" class="fullButton" primary :disabled="action.disabled" @click.stop="action.handler"><i :class="action.icon" style="margin-right: 6px;"></i>{{ action.text }}</MkButton>
 | 
				
			||||||
				<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
 | 
									<button v-else v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" :disabled="action.disabled" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
 | 
				
			||||||
			</template>
 | 
								</template>
 | 
				
			||||||
		</template>
 | 
							</template>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
| 
						 | 
					@ -56,6 +56,7 @@ const props = defineProps<{
 | 
				
			||||||
		text: string;
 | 
							text: string;
 | 
				
			||||||
		icon: string;
 | 
							icon: string;
 | 
				
			||||||
		asFullButton?: boolean;
 | 
							asFullButton?: boolean;
 | 
				
			||||||
 | 
							disabled?: boolean;
 | 
				
			||||||
		handler: (ev: MouseEvent) => void;
 | 
							handler: (ev: MouseEvent) => void;
 | 
				
			||||||
	}[];
 | 
						}[];
 | 
				
			||||||
	thin?: boolean;
 | 
						thin?: boolean;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
			<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
 | 
								<MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo>
 | 
				
			||||||
			<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
 | 
								<MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<MkSelect v-model="announcementsStatus">
 | 
				
			||||||
 | 
									<template #label>{{ i18n.ts.filter }}</template>
 | 
				
			||||||
 | 
									<option value="active">{{ i18n.ts.active }}</option>
 | 
				
			||||||
 | 
									<option value="archived">{{ i18n.ts.archived }}</option>
 | 
				
			||||||
 | 
								</MkSelect>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<MkLoading v-if="loading"/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<template v-else>
 | 
				
			||||||
				<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
 | 
									<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
 | 
				
			||||||
					<template #label>{{ announcement.title }}</template>
 | 
										<template #label>{{ announcement.title }}</template>
 | 
				
			||||||
					<template #icon>
 | 
										<template #icon>
 | 
				
			||||||
| 
						 | 
					@ -57,24 +66,28 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
				
			||||||
						<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
 | 
											<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
 | 
				
			||||||
						<div class="buttons _buttons">
 | 
											<div class="buttons _buttons">
 | 
				
			||||||
							<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 | 
												<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
 | 
				
			||||||
						<MkButton v-if="announcement.id != null" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
 | 
												<MkButton v-if="announcement.id != null && announcement.isActive" class="button" inline @click="archive(announcement)"><i class="ti ti-check"></i> {{ i18n.ts._announcement.end }} ({{ i18n.ts.archive }})</MkButton>
 | 
				
			||||||
 | 
												<MkButton v-if="announcement.id != null && !announcement.isActive" class="button" inline @click="unarchive(announcement)"><i class="ti ti-restore"></i> {{ i18n.ts.unarchive }}</MkButton>
 | 
				
			||||||
							<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 | 
												<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</MkFolder>
 | 
									</MkFolder>
 | 
				
			||||||
 | 
									<MkLoading v-if="loadingMore"/>
 | 
				
			||||||
				<MkButton class="button" @click="more()">
 | 
									<MkButton class="button" @click="more()">
 | 
				
			||||||
					<i class="ti ti-reload"></i>{{ i18n.ts.more }}
 | 
										<i class="ti ti-reload"></i>{{ i18n.ts.more }}
 | 
				
			||||||
				</MkButton>
 | 
									</MkButton>
 | 
				
			||||||
 | 
								</template>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</MkSpacer>
 | 
						</MkSpacer>
 | 
				
			||||||
</MkStickyContainer>
 | 
					</MkStickyContainer>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { ref, computed } from 'vue';
 | 
					import { ref, computed, watch } from 'vue';
 | 
				
			||||||
import XHeader from './_header_.vue';
 | 
					import XHeader from './_header_.vue';
 | 
				
			||||||
import MkButton from '@/components/MkButton.vue';
 | 
					import MkButton from '@/components/MkButton.vue';
 | 
				
			||||||
import MkInput from '@/components/MkInput.vue';
 | 
					import MkInput from '@/components/MkInput.vue';
 | 
				
			||||||
 | 
					import MkSelect from '@/components/MkSelect.vue';
 | 
				
			||||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
					import MkSwitch from '@/components/MkSwitch.vue';
 | 
				
			||||||
import MkRadios from '@/components/MkRadios.vue';
 | 
					import MkRadios from '@/components/MkRadios.vue';
 | 
				
			||||||
import MkInfo from '@/components/MkInfo.vue';
 | 
					import MkInfo from '@/components/MkInfo.vue';
 | 
				
			||||||
| 
						 | 
					@ -85,11 +98,22 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
 | 
				
			||||||
import MkFolder from '@/components/MkFolder.vue';
 | 
					import MkFolder from '@/components/MkFolder.vue';
 | 
				
			||||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
					import MkTextarea from '@/components/MkTextarea.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const announcementsStatus = ref<'active' | 'archived'>('active');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const loading = ref(true);
 | 
				
			||||||
 | 
					const loadingMore = ref(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const announcements = ref<any[]>([]);
 | 
					const announcements = ref<any[]>([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
misskeyApi('admin/announcements/list').then(announcementResponse => {
 | 
					watch(announcementsStatus, (to) => {
 | 
				
			||||||
 | 
						loading.value = true;
 | 
				
			||||||
 | 
						misskeyApi('admin/announcements/list', {
 | 
				
			||||||
 | 
							status: to,
 | 
				
			||||||
 | 
						}).then(announcementResponse => {
 | 
				
			||||||
		announcements.value = announcementResponse;
 | 
							announcements.value = announcementResponse;
 | 
				
			||||||
 | 
							loading.value = false;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					}, { immediate: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function add() {
 | 
					function add() {
 | 
				
			||||||
	announcements.value.unshift({
 | 
						announcements.value.unshift({
 | 
				
			||||||
| 
						 | 
					@ -125,6 +149,14 @@ async function archive(announcement) {
 | 
				
			||||||
	refresh();
 | 
						refresh();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function unarchive(announcement) {
 | 
				
			||||||
 | 
						await os.apiWithDialog('admin/announcements/update', {
 | 
				
			||||||
 | 
							...announcement,
 | 
				
			||||||
 | 
							isActive: true,
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						refresh();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function save(announcement) {
 | 
					async function save(announcement) {
 | 
				
			||||||
	if (announcement.id == null) {
 | 
						if (announcement.id == null) {
 | 
				
			||||||
		await os.apiWithDialog('admin/announcements/create', announcement);
 | 
							await os.apiWithDialog('admin/announcements/create', announcement);
 | 
				
			||||||
| 
						 | 
					@ -135,24 +167,32 @@ async function save(announcement) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function more() {
 | 
					function more() {
 | 
				
			||||||
	misskeyApi('admin/announcements/list', { untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id }).then(announcementResponse => {
 | 
						loadingMore.value = true;
 | 
				
			||||||
 | 
						misskeyApi('admin/announcements/list', {
 | 
				
			||||||
 | 
							status: announcementsStatus.value,
 | 
				
			||||||
 | 
							untilId: announcements.value.reduce((acc, announcement) => announcement.id != null ? announcement : acc).id
 | 
				
			||||||
 | 
						}).then(announcementResponse => {
 | 
				
			||||||
		announcements.value = announcements.value.concat(announcementResponse);
 | 
							announcements.value = announcements.value.concat(announcementResponse);
 | 
				
			||||||
 | 
							loadingMore.value = false;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function refresh() {
 | 
					function refresh() {
 | 
				
			||||||
	misskeyApi('admin/announcements/list').then(announcementResponse => {
 | 
						loading.value = true;
 | 
				
			||||||
 | 
						misskeyApi('admin/announcements/list', {
 | 
				
			||||||
 | 
							status: announcementsStatus.value,
 | 
				
			||||||
 | 
						}).then(announcementResponse => {
 | 
				
			||||||
		announcements.value = announcementResponse;
 | 
							announcements.value = announcementResponse;
 | 
				
			||||||
 | 
							loading.value = false;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
refresh();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const headerActions = computed(() => [{
 | 
					const headerActions = computed(() => [{
 | 
				
			||||||
	asFullButton: true,
 | 
						asFullButton: true,
 | 
				
			||||||
	icon: 'ti ti-plus',
 | 
						icon: 'ti ti-plus',
 | 
				
			||||||
	text: i18n.ts.add,
 | 
						text: i18n.ts.add,
 | 
				
			||||||
	handler: add,
 | 
						handler: add,
 | 
				
			||||||
 | 
						disabled: announcementsStatus.value === 'archived',
 | 
				
			||||||
}]);
 | 
					}]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const headerTabs = computed(() => []);
 | 
					const headerTabs = computed(() => []);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6091,6 +6091,11 @@ export type operations = {
 | 
				
			||||||
          untilId?: string;
 | 
					          untilId?: string;
 | 
				
			||||||
          /** Format: misskey:id */
 | 
					          /** Format: misskey:id */
 | 
				
			||||||
          userId?: string | null;
 | 
					          userId?: string | null;
 | 
				
			||||||
 | 
					          /**
 | 
				
			||||||
 | 
					           * @default active
 | 
				
			||||||
 | 
					           * @enum {string}
 | 
				
			||||||
 | 
					           */
 | 
				
			||||||
 | 
					          status?: 'all' | 'active' | 'archived';
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue