diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2024-07-30 13:11:06 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-07-30 13:11:06 +0900 |
| commit | 738b3ea43b059b103deca0b1a33071ae256ef79f (patch) | |
| tree | b47248996be4aa737cedcb62bdc04aa7aa63fcd2 /packages/frontend/src/components | |
| parent | enhance: 管理画面でアーカイブにしたお知らせを表示・編... (diff) | |
| download | misskey-738b3ea43b059b103deca0b1a33071ae256ef79f.tar.gz misskey-738b3ea43b059b103deca0b1a33071ae256ef79f.tar.bz2 misskey-738b3ea43b059b103deca0b1a33071ae256ef79f.zip | |
enhance(frontend): デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように (#14104)
* enhance(frontend): デッキのアンテナ・リスト選択画面からそれぞれを新規作成できるように
* Update Changelog
* fix
* fix
* lint
* add story
* typo
ねぼけていた
* Update antenna-column.vue
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
Diffstat (limited to 'packages/frontend/src/components')
5 files changed, 378 insertions, 5 deletions
diff --git a/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts new file mode 100644 index 0000000000..1749e07a4e --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditor.stories.impl.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkAntennaEditor from './MkAntennaEditor.vue'; +export const Default = { + render(args) { + return { + components: { + MkAntennaEditor, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + created: action('created'), + updated: action('updated'), + deleted: action('deleted'), + }; + }, + }, + template: '<MkAntennaEditor v-bind="props" v-on="events" />', + }; + }, + args: { + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/antennas/create', async ({ request }) => { + action('POST /api/antennas/create')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/update', async ({ request }) => { + action('POST /api/antennas/update')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/delete', async ({ request }) => { + action('POST /api/antennas/delete')(await request.json()); + return HttpResponse.json(); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkAntennaEditor>; diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue new file mode 100644 index 0000000000..cb7ee3d6ca --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditor.vue @@ -0,0 +1,175 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkSpacer :contentMax="700"> + <div> + <div class="_gaps_m"> + <MkInput v-model="name"> + <template #label>{{ i18n.ts.name }}</template> + </MkInput> + <MkSelect v-model="src"> + <template #label>{{ i18n.ts.antennaSource }}</template> + <option value="all">{{ i18n.ts._antennaSources.all }}</option> + <!--<option value="home">{{ i18n.ts._antennaSources.homeTimeline }}</option>--> + <option value="users">{{ i18n.ts._antennaSources.users }}</option> + <!--<option value="list">{{ i18n.ts._antennaSources.userList }}</option>--> + <option value="users_blacklist">{{ i18n.ts._antennaSources.userBlacklist }}</option> + </MkSelect> + <MkSelect v-if="src === 'list'" v-model="userListId"> + <template #label>{{ i18n.ts.userList }}</template> + <option v-for="list in userLists" :key="list.id" :value="list.id">{{ list.name }}</option> + </MkSelect> + <MkTextarea v-else-if="src === 'users' || src === 'users_blacklist'" v-model="users"> + <template #label>{{ i18n.ts.users }}</template> + <template #caption>{{ i18n.ts.antennaUsersDescription }} <button class="_textButton" @click="addUser">{{ i18n.ts.addUser }}</button></template> + </MkTextarea> + <MkSwitch v-model="excludeBots">{{ i18n.ts.antennaExcludeBots }}</MkSwitch> + <MkSwitch v-model="withReplies">{{ i18n.ts.withReplies }}</MkSwitch> + <MkTextarea v-model="keywords"> + <template #label>{{ i18n.ts.antennaKeywords }}</template> + <template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template> + </MkTextarea> + <MkTextarea v-model="excludeKeywords"> + <template #label>{{ i18n.ts.antennaExcludeKeywords }}</template> + <template #caption>{{ i18n.ts.antennaKeywordsDescription }}</template> + </MkTextarea> + <MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch> + <MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch> + <MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch> + </div> + <div :class="$style.actions"> + <div class="_buttons"> + <MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton> + <MkButton v-if="initialAntenna.id != null" inline danger @click="deleteAntenna()"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton> + </div> + </div> + </div> +</MkSpacer> +</template> + +<script lang="ts" setup> +import { watch, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkButton from '@/components/MkButton.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkTextarea from '@/components/MkTextarea.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkSwitch from '@/components/MkSwitch.vue'; +import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; +import { i18n } from '@/i18n.js'; +import { deepMerge } from '@/scripts/merge.js'; +import type { DeepPartial } from '@/scripts/merge.js'; + +type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & { + id?: string; + createdAt?: string; + updatedAt?: string; +}; + +const props = defineProps<{ + antenna?: DeepPartial<PartialAllowedAntenna>; +}>(); + +const initialAntenna = deepMerge<PartialAllowedAntenna>(props.antenna ?? {}, { + name: '', + src: 'all', + userListId: null, + users: [], + keywords: [], + excludeKeywords: [], + excludeBots: false, + withReplies: false, + caseSensitive: false, + localOnly: false, + withFile: false, + isActive: true, + hasUnreadNote: false, + notify: false, +}); + +const emit = defineEmits<{ + (ev: 'created', newAntenna: Misskey.entities.Antenna): void, + (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void, + (ev: 'deleted'): void, +}>(); + +const name = ref<string>(initialAntenna.name); +const src = ref<Misskey.entities.AntennasCreateRequest['src']>(initialAntenna.src); +const userListId = ref<string | null>(initialAntenna.userListId); +const users = ref<string>(initialAntenna.users.join('\n')); +const keywords = ref<string>(initialAntenna.keywords.map(x => x.join(' ')).join('\n')); +const excludeKeywords = ref<string>(initialAntenna.excludeKeywords.map(x => x.join(' ')).join('\n')); +const caseSensitive = ref<boolean>(initialAntenna.caseSensitive); +const localOnly = ref<boolean>(initialAntenna.localOnly); +const excludeBots = ref<boolean>(initialAntenna.excludeBots); +const withReplies = ref<boolean>(initialAntenna.withReplies); +const withFile = ref<boolean>(initialAntenna.withFile); +const userLists = ref<Misskey.entities.UserList[] | null>(null); + +watch(() => src.value, async () => { + if (src.value === 'list' && userLists.value === null) { + userLists.value = await misskeyApi('users/lists/list'); + } +}); + +async function saveAntenna() { + const antennaData = { + name: name.value, + src: src.value, + userListId: userListId.value, + excludeBots: excludeBots.value, + withReplies: withReplies.value, + withFile: withFile.value, + caseSensitive: caseSensitive.value, + localOnly: localOnly.value, + users: users.value.trim().split('\n').map(x => x.trim()), + keywords: keywords.value.trim().split('\n').map(x => x.trim().split(' ')), + excludeKeywords: excludeKeywords.value.trim().split('\n').map(x => x.trim().split(' ')), + }; + + if (initialAntenna.id == null) { + const res = await os.apiWithDialog('antennas/create', antennaData); + emit('created', res); + } else { + const res = await os.apiWithDialog('antennas/update', { ...antennaData, antennaId: initialAntenna.id }); + emit('updated', res); + } +} + +async function deleteAntenna() { + if (initialAntenna.id == null) return; + + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.tsx.removeAreYouSure({ x: initialAntenna.name }), + }); + if (canceled) return; + + await misskeyApi('antennas/delete', { + antennaId: initialAntenna.id, + }); + + os.success(); + emit('deleted'); +} + +function addUser() { + os.selectUser({ includeSelf: true }).then(user => { + users.value = users.value.trim(); + users.value += '\n@' + Misskey.acct.toString(user as any); + users.value = users.value.trim(); + }); +} +</script> + +<style lang="scss" module> +.actions { + margin-top: 16px; + padding: 24px 0; + border-top: solid 0.5px var(--divider); +} +</style> diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts new file mode 100644 index 0000000000..1c6ca83b47 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditorDialog.stories.impl.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { action } from '@storybook/addon-actions'; +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import { commonHandlers } from '../../.storybook/mocks.js'; +import MkAntennaEditorDialog from './MkAntennaEditorDialog.vue'; +export const Default = { + render(args) { + return { + components: { + MkAntennaEditorDialog, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + events() { + return { + created: action('created'), + updated: action('updated'), + deleted: action('deleted'), + closed: action('closed'), + }; + }, + }, + template: '<MkAntennaEditorDialog v-bind="props" v-on="events" />', + }; + }, + args: { + }, + parameters: { + layout: 'centered', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/antennas/create', async ({ request }) => { + action('POST /api/antennas/create')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/update', async ({ request }) => { + action('POST /api/antennas/update')(await request.json()); + return HttpResponse.json({}); + }), + http.post('/api/antennas/delete', async ({ request }) => { + action('POST /api/antennas/delete')(await request.json()); + return HttpResponse.json(); + }), + ], + }, + }, +} satisfies StoryObj<typeof MkAntennaEditorDialog>; diff --git a/packages/frontend/src/components/MkAntennaEditorDialog.vue b/packages/frontend/src/components/MkAntennaEditorDialog.vue new file mode 100644 index 0000000000..6d815d29f3 --- /dev/null +++ b/packages/frontend/src/components/MkAntennaEditorDialog.vue @@ -0,0 +1,63 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :withOkButton="false" + :width="500" + :height="550" + @close="close()" + @closed="emit('closed')" +> + <template #header>{{ antenna == null ? i18n.ts.createAntenna : i18n.ts.editAntenna }}</template> + <XAntennaEditor + :antenna="antenna" + @created="onAntennaCreated" + @updated="onAntennaUpdated" + @deleted="onAntennaDeleted" + /> +</MkModalWindow> +</template> + +<script lang="ts" setup> +import { shallowRef } from 'vue'; +import * as Misskey from 'misskey-js'; +import MkModalWindow from '@/components/MkModalWindow.vue'; +import XAntennaEditor from '@/components/MkAntennaEditor.vue'; +import { i18n } from '@/i18n.js'; + +defineProps<{ + antenna?: Misskey.entities.Antenna; +}>(); + +const emit = defineEmits<{ + (ev: 'created', newAntenna: Misskey.entities.Antenna): void, + (ev: 'updated', editedAntenna: Misskey.entities.Antenna): void, + (ev: 'deleted'): void, + (ev: 'closed'): void, +}>(); + +const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); + +function onAntennaCreated(newAntenna: Misskey.entities.Antenna) { + emit('created', newAntenna); + dialog.value?.close(); +} + +function onAntennaUpdated(editedAntenna: Misskey.entities.Antenna) { + emit('updated', editedAntenna); + dialog.value?.close(); +} + +function onAntennaDeleted() { + emit('deleted'); + dialog.value?.close(); +} + +function close() { + dialog.value?.close(); +} +</script> diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 5c3c6aa51d..16cf5b1b75 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -36,7 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> <MkSelect v-if="select" v-model="selectedValue" autofocus> <template v-if="select.items"> - <option v-for="item in select.items" :value="item.value">{{ item.text }}</option> + <template v-for="item in select.items"> + <optgroup v-if="'sectionTitle' in item" :label="item.sectionTitle"> + <option v-for="subItem in item.items" :value="subItem.value">{{ subItem.text }}</option> + </optgroup> + <option v-else :value="item.value">{{ item.text }}</option> + </template> </template> </MkSelect> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> @@ -67,11 +72,16 @@ type Input = { maxLength?: number; }; +type SelectItem = { + value: any; + text: string; +}; + type Select = { - items: { - value: any; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + })[]; default: string | null; }; |