diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-07-30 13:02:03 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-30 13:02:03 +0900 |
| commit | de3ddb5b4403d02b8cb7671b9cf26e87ce9a2d17 (patch) | |
| tree | a9578c25dda99adfcc59da234142049c7e39ba87 | |
| parent | fix(backend): type(schema) of reactionAcceptance was wrong (#14317) (diff) | |
| download | sharkey-de3ddb5b4403d02b8cb7671b9cf26e87ce9a2d17.tar.gz sharkey-de3ddb5b4403d02b8cb7671b9cf26e87ce9a2d17.tar.bz2 sharkey-de3ddb5b4403d02b8cb7671b9cf26e87ce9a2d17.zip | |
enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように (#14286)
* enhance: 管理画面でアーカイブにしたお知らせを表示できるように
* Update Changelog
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | locales/index.d.ts | 8 | ||||
| -rw-r--r-- | locales/ja-JP.yml | 2 | ||||
| -rw-r--r-- | packages/backend/src/server/api/endpoints/admin/announcements/list.ts | 9 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/_header_.vue | 5 | ||||
| -rw-r--r-- | packages/frontend/src/pages/admin/announcements.vue | 160 | ||||
| -rw-r--r-- | packages/misskey-js/src/autogen/types.ts | 5 |
7 files changed, 127 insertions, 63 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c233e358..8da94f3749 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Feat: ユーザーのアイコン/バナーの変更可否をロールで設定可能に - 変更不可となっていても、設定済みのものを解除してデフォルト画像に戻すことは出来ます - Feat: ユーザ作成時にSystemWebhookを送信可能に #14281 +- Enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように - Fix: 配信停止したインスタンス一覧が見れなくなる問題を修正 - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 diff --git a/locales/index.d.ts b/locales/index.d.ts index 2b340ecbb5..d54b752312 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4441,6 +4441,14 @@ export interface Locale extends ILocale { */ "archive": string; /** + * アーカイブ済み + */ + "archived": string; + /** + * アーカイブ解除 + */ + "unarchive": string; + /** * {name}をアーカイブしますか? */ "channelArchiveConfirmTitle": ParameterizedString<"name">; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index f0f849fb38..d8531070fe 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1106,6 +1106,8 @@ preservedUsernames: "予約ユーザー名" preservedUsernamesDescription: "予約するユーザー名を改行で列挙します。ここで指定されたユーザー名はアカウント作成時に使えなくなりますが、管理者によるアカウント作成時はこの制限を受けません。また、既に存在するアカウントも影響を受けません。" createNoteFromTheFile: "このファイルからノートを作成" archive: "アーカイブ" +archived: "アーカイブ済み" +unarchive: "アーカイブ解除" channelArchiveConfirmTitle: "{name}をアーカイブしますか?" channelArchiveConfirmDescription: "アーカイブすると、チャンネル一覧や検索結果に表示されなくなり、新たな書き込みもできなくなります。" thisChannelArchived: "このチャンネルはアーカイブされています。" diff --git a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts index 87eaad31a3..7596bf44e3 100644 --- a/packages/backend/src/server/api/endpoints/admin/announcements/list.ts +++ b/packages/backend/src/server/api/endpoints/admin/announcements/list.ts @@ -69,6 +69,7 @@ export const paramDef = { sinceId: { type: 'string', format: 'misskey:id' }, untilId: { type: 'string', format: 'misskey:id' }, userId: { type: 'string', format: 'misskey:id', nullable: true }, + status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' }, }, required: [], } as const; @@ -87,7 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- ) { super(meta, paramDef, async (ps, me) => { const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId); - query.andWhere('announcement.isActive = true'); + + if (ps.status === 'archived') { + query.andWhere('announcement.isActive = false'); + } else if (ps.status === 'active') { + query.andWhere('announcement.isActive = true'); + } + if (ps.userId) { query.andWhere('announcement.userId = :userId', { userId: ps.userId }); } else { diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue index c5a9609e6e..b88f078598 100644 --- a/packages/frontend/src/pages/admin/_header_.vue +++ b/packages/frontend/src/pages/admin/_header_.vue @@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div class="buttons right"> <template v-if="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> - <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> + <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 }" :disabled="action.disabled" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> </template> </template> </div> @@ -56,6 +56,7 @@ const props = defineProps<{ text: string; icon: string; asFullButton?: boolean; + disabled?: boolean; handler: (ev: MouseEvent) => void; }[]; thin?: boolean; diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue index e7fb62ec1d..b9e09c8d03 100644 --- a/packages/frontend/src/pages/admin/announcements.vue +++ b/packages/frontend/src/pages/admin/announcements.vue @@ -11,70 +11,83 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo>{{ i18n.ts._announcement.shouldNotBeUsedToPresentPermanentInfo }}</MkInfo> <MkInfo v-if="announcements.length > 5" warn>{{ i18n.ts._announcement.tooManyActiveAnnouncementDescription }}</MkInfo> - <MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null"> - <template #label>{{ announcement.title }}</template> - <template #icon> - <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> - <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> - <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> - <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> - </template> - <template #caption>{{ announcement.text }}</template> + <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> - <div class="_gaps_m"> - <MkInput v-model="announcement.title"> - <template #label>{{ i18n.ts.title }}</template> - </MkInput> - <MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true"> - <template #label>{{ i18n.ts.text }}</template> - </MkTextarea> - <MkInput v-model="announcement.imageUrl" type="url"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <MkRadios v-model="announcement.icon"> - <template #label>{{ i18n.ts.icon }}</template> - <option value="info"><i class="ti ti-info-circle"></i></option> - <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> - <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> - <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> - </MkRadios> - <MkRadios v-model="announcement.display"> - <template #label>{{ i18n.ts.display }}</template> - <option value="normal">{{ i18n.ts.normal }}</option> - <option value="banner">{{ i18n.ts.banner }}</option> - <option value="dialog">{{ i18n.ts.dialog }}</option> - </MkRadios> - <MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo> - <MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription"> - {{ i18n.ts._announcement.forExistingUsers }} - </MkSwitch> - <MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription"> - {{ i18n.ts._announcement.silence }} - </MkSwitch> - <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> - {{ i18n.ts._announcement.needConfirmationToRead }} - </MkSwitch> - <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> - <div class="buttons _buttons"> - <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" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + <MkLoading v-if="loading"/> + + <template v-else> + <MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null"> + <template #label>{{ announcement.title }}</template> + <template #icon> + <i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i> + <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i> + <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i> + <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i> + </template> + <template #caption>{{ announcement.text }}</template> + + <div class="_gaps_m"> + <MkInput v-model="announcement.title"> + <template #label>{{ i18n.ts.title }}</template> + </MkInput> + <MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true"> + <template #label>{{ i18n.ts.text }}</template> + </MkTextarea> + <MkInput v-model="announcement.imageUrl" type="url"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <MkRadios v-model="announcement.icon"> + <template #label>{{ i18n.ts.icon }}</template> + <option value="info"><i class="ti ti-info-circle"></i></option> + <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option> + <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option> + <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option> + </MkRadios> + <MkRadios v-model="announcement.display"> + <template #label>{{ i18n.ts.display }}</template> + <option value="normal">{{ i18n.ts.normal }}</option> + <option value="banner">{{ i18n.ts.banner }}</option> + <option value="dialog">{{ i18n.ts.dialog }}</option> + </MkRadios> + <MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo> + <MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription"> + {{ i18n.ts._announcement.forExistingUsers }} + </MkSwitch> + <MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription"> + {{ i18n.ts._announcement.silence }} + </MkSwitch> + <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> + {{ i18n.ts._announcement.needConfirmationToRead }} + </MkSwitch> + <p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p> + <div class="buttons _buttons"> + <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 && 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> + </div> </div> - </div> - </MkFolder> - <MkButton class="button" @click="more()"> - <i class="ti ti-reload"></i>{{ i18n.ts.more }} - </MkButton> + </MkFolder> + <MkLoading v-if="loadingMore"/> + <MkButton class="button" @click="more()"> + <i class="ti ti-reload"></i>{{ i18n.ts.more }} + </MkButton> + </template> </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> -import { ref, computed } from 'vue'; +import { ref, computed, watch } from 'vue'; import XHeader from './_header_.vue'; import MkButton from '@/components/MkButton.vue'; import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import MkRadios from '@/components/MkRadios.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 MkTextarea from '@/components/MkTextarea.vue'; +const announcementsStatus = ref<'active' | 'archived'>('active'); + +const loading = ref(true); +const loadingMore = ref(false); + const announcements = ref<any[]>([]); -misskeyApi('admin/announcements/list').then(announcementResponse => { - announcements.value = announcementResponse; -}); +watch(announcementsStatus, (to) => { + loading.value = true; + misskeyApi('admin/announcements/list', { + status: to, + }).then(announcementResponse => { + announcements.value = announcementResponse; + loading.value = false; + }); +}, { immediate: true }); function add() { announcements.value.unshift({ @@ -125,6 +149,14 @@ async function archive(announcement) { refresh(); } +async function unarchive(announcement) { + await os.apiWithDialog('admin/announcements/update', { + ...announcement, + isActive: true, + }); + refresh(); +} + async function save(announcement) { if (announcement.id == null) { await os.apiWithDialog('admin/announcements/create', announcement); @@ -135,24 +167,32 @@ async function save(announcement) { } 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); + loadingMore.value = false; }); } function refresh() { - misskeyApi('admin/announcements/list').then(announcementResponse => { + loading.value = true; + misskeyApi('admin/announcements/list', { + status: announcementsStatus.value, + }).then(announcementResponse => { announcements.value = announcementResponse; + loading.value = false; }); } -refresh(); - const headerActions = computed(() => [{ asFullButton: true, icon: 'ti ti-plus', text: i18n.ts.add, handler: add, + disabled: announcementsStatus.value === 'archived', }]); const headerTabs = computed(() => []); diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 52a571b694..4cfe92147a 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -6091,6 +6091,11 @@ export type operations = { untilId?: string; /** Format: misskey:id */ userId?: string | null; + /** + * @default active + * @enum {string} + */ + status?: 'all' | 'active' | 'archived'; }; }; }; |