enhance: 管理画面でアーカイブにしたお知らせを表示・編集できるように (#14286)
* enhance: 管理画面でアーカイブにしたお知らせを表示できるように * Update Changelog
This commit is contained in:
parent
b44313fe3c
commit
de3ddb5b44
|
@ -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が使用できなくなる問題を修正
|
||||||
|
|
|
@ -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);
|
||||||
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) {
|
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,70 +11,83 @@ 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>
|
||||||
|
|
||||||
<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
|
<MkSelect v-model="announcementsStatus">
|
||||||
<template #label>{{ announcement.title }}</template>
|
<template #label>{{ i18n.ts.filter }}</template>
|
||||||
<template #icon>
|
<option value="active">{{ i18n.ts.active }}</option>
|
||||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
<option value="archived">{{ i18n.ts.archived }}</option>
|
||||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
</MkSelect>
|
||||||
<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">
|
<MkLoading v-if="loading"/>
|
||||||
<MkInput v-model="announcement.title">
|
|
||||||
<template #label>{{ i18n.ts.title }}</template>
|
<template v-else>
|
||||||
</MkInput>
|
<MkFolder v-for="announcement in announcements" :key="announcement.id ?? announcement._id" :defaultOpen="announcement.id == null">
|
||||||
<MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true">
|
<template #label>{{ announcement.title }}</template>
|
||||||
<template #label>{{ i18n.ts.text }}</template>
|
<template #icon>
|
||||||
</MkTextarea>
|
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||||
<MkInput v-model="announcement.imageUrl" type="url">
|
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||||
<template #label>{{ i18n.ts.imageUrl }}</template>
|
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||||
</MkInput>
|
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||||
<MkRadios v-model="announcement.icon">
|
</template>
|
||||||
<template #label>{{ i18n.ts.icon }}</template>
|
<template #caption>{{ announcement.text }}</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>
|
<div class="_gaps_m">
|
||||||
<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
|
<MkInput v-model="announcement.title">
|
||||||
<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
|
<template #label>{{ i18n.ts.title }}</template>
|
||||||
</MkRadios>
|
</MkInput>
|
||||||
<MkRadios v-model="announcement.display">
|
<MkTextarea v-model="announcement.text" mfmAutocomplete :mfmPreview="true">
|
||||||
<template #label>{{ i18n.ts.display }}</template>
|
<template #label>{{ i18n.ts.text }}</template>
|
||||||
<option value="normal">{{ i18n.ts.normal }}</option>
|
</MkTextarea>
|
||||||
<option value="banner">{{ i18n.ts.banner }}</option>
|
<MkInput v-model="announcement.imageUrl" type="url">
|
||||||
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
<template #label>{{ i18n.ts.imageUrl }}</template>
|
||||||
</MkRadios>
|
</MkInput>
|
||||||
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
|
<MkRadios v-model="announcement.icon">
|
||||||
<MkSwitch v-model="announcement.forExistingUsers" :helpText="i18n.ts._announcement.forExistingUsersDescription">
|
<template #label>{{ i18n.ts.icon }}</template>
|
||||||
{{ i18n.ts._announcement.forExistingUsers }}
|
<option value="info"><i class="ti ti-info-circle"></i></option>
|
||||||
</MkSwitch>
|
<option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
|
||||||
<MkSwitch v-model="announcement.silence" :helpText="i18n.ts._announcement.silenceDescription">
|
<option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
|
||||||
{{ i18n.ts._announcement.silence }}
|
<option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
|
||||||
</MkSwitch>
|
</MkRadios>
|
||||||
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
|
<MkRadios v-model="announcement.display">
|
||||||
{{ i18n.ts._announcement.needConfirmationToRead }}
|
<template #label>{{ i18n.ts.display }}</template>
|
||||||
</MkSwitch>
|
<option value="normal">{{ i18n.ts.normal }}</option>
|
||||||
<p v-if="announcement.reads">{{ i18n.tsx.nUsersRead({ n: announcement.reads }) }}</p>
|
<option value="banner">{{ i18n.ts.banner }}</option>
|
||||||
<div class="buttons _buttons">
|
<option value="dialog">{{ i18n.ts.dialog }}</option>
|
||||||
<MkButton class="button" inline primary @click="save(announcement)"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
</MkRadios>
|
||||||
<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>
|
<MkInfo v-if="announcement.display === 'dialog'" warn>{{ i18n.ts._announcement.dialogAnnouncementUxWarn }}</MkInfo>
|
||||||
<MkButton v-if="announcement.id != null" class="button" inline danger @click="del(announcement)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
|
<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>
|
||||||
</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) => {
|
||||||
announcements.value = announcementResponse;
|
loading.value = true;
|
||||||
});
|
misskeyApi('admin/announcements/list', {
|
||||||
|
status: to,
|
||||||
|
}).then(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…
Reference in New Issue