Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

クリップフォルダの使いやすさ向上 #4197

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/components/Main/MainView/MessageElement/MessageTools.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@
:class="$style.icon"
@click="toggleStampPicker"
/>
<a-icon
mdi
:name="clipIconName"
:size="28"
:class="$style.icon"
:data-clipped="$boolAttr(isClipped)"
@click="openClipCreateModal"
/>
<a-icon
:class="$style.icon"
:size="28"
Expand All @@ -82,7 +90,7 @@

<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { StampId, MessageId } from '/@/types/entity-ids'
import type { StampId, MessageId, ClipFolderId } from '/@/types/entity-ids'
import { useStampPickerInvoker } from '/@/store/ui/stampPicker'
import { useResponsiveStore } from '/@/store/ui/responsive'
import apis from '/@/lib/apis'
Expand All @@ -95,6 +103,7 @@ import AStamp from '/@/components/UI/AStamp.vue'
import MessageContextMenu from './MessageContextMenu.vue'
import useToggle from '/@/composables/utils/useToggle'
import { useStampHistory } from '/@/store/domain/stampHistory'
import { useCreateClip } from '/@/composables/clips/createClip'

const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -169,6 +178,24 @@ const onDotsClick = (e: MouseEvent) => {
})
}

const DEFAULT_CLIP_FOLDER_ID = '201fcb38-3dbe-4a53-8c0c-37b47bed9985' // FIXME: あとで直す
const clippingFolderIds = ref(new Set<ClipFolderId>())
const isClipped = computed(() =>
clippingFolderIds.value.has(DEFAULT_CLIP_FOLDER_ID)
)

apis.getMessageClips(props.messageId).then(res => {
clippingFolderIds.value = new Set(res.data.map(c => c.folderId))
})
const clipIconName = computed(() => {
return isClipped.value ? 'bookmark-check' : 'bookmark'
})

const { toggleClip } = useCreateClip(props.messageId, clippingFolderIds)
const openClipCreateModal = () => {
toggleClip(DEFAULT_CLIP_FOLDER_ID)
}

const { isMobile } = useResponsiveStore()

const { value: showQuickReaction, toggle: toggleQuickReaction } = useToggle(
Expand Down Expand Up @@ -231,6 +258,9 @@ const { value: showQuickReaction, toggle: toggleQuickReaction } = useToggle(
&:hover {
@include background-secondary;
}
&[data-clipped] {
@include color-accent-primary;
}
}

.stampListItem {
Expand Down
86 changes: 28 additions & 58 deletions src/components/Modal/ClipCreateModal/ClipCreateModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,31 @@
<inline-markdown :class="$style.subtitle" :content="messageContent" />
</template>
<template #default>
<clip-folder-element
v-for="clipFolder in sortedClipFolders"
:key="clipFolder.id"
:folder-name="clipFolder.name"
:is-selected="isSelected.has(clipFolder.id)"
@click="toggleClip(clipFolder.id)"
/>
<div>
<clip-folder-element
v-for="clipFolder in sortedClipFolders"
:key="clipFolder.id"
:folder-name="clipFolder.name"
:is-selected="isSelected.has(clipFolder.id)"
@click="toggleClip(clipFolder.id)"
/>
<clip-folder-new @create-clip-folder="handleCreateClipFolder" />
</div>
</template>
</modal-frame>
</template>

<script lang="ts">
import type { Ref } from 'vue'
import { computed, ref } from 'vue'
import apis from '/@/lib/apis'
import type { MessageId, ClipFolderId } from '/@/types/entity-ids'
import { useToastStore } from '/@/store/ui/toast'
import type { AxiosError } from 'axios'
import { useMessagesStore } from '/@/store/entities/messages'
import useSortedClipFolders from '/@/composables/clips/useSortedClipFolders'

const useCreateClip = (
props: { messageId: MessageId },
isSelected: Ref<Set<ClipFolderId>>
) => {
const { addSuccessToast, addErrorToast } = useToastStore()

const createClip = async (clipFolderId: ClipFolderId) => {
try {
await apis.clipMessage(clipFolderId, {
messageId: props.messageId
})
isSelected.value.add(clipFolderId)
addSuccessToast('クリップフォルダに追加しました')
} catch (e) {
if ((e as AxiosError).response?.status === 409) {
isSelected.value.add(clipFolderId)
addErrorToast('すでに追加されています')
return
} else {
addErrorToast('追加に失敗しました')
}
throw e
}
}
const deleteClip = async (clipFolderId: ClipFolderId) => {
await apis.unclipMessage(clipFolderId, props.messageId)
isSelected.value.delete(clipFolderId)
addSuccessToast('クリップフォルダから削除しました')
}
const toggleClip = async (clipFolderId: ClipFolderId) => {
if (isSelected.value.has(clipFolderId)) {
await deleteClip(clipFolderId)
} else {
await createClip(clipFolderId)
}
}
return { toggleClip }
}
</script>

<script lang="ts" setup>
import ModalFrame from '../Common/ModalFrame.vue'
import ClipFolderElement from './ClipFolderElement.vue'
import InlineMarkdown from '/@/components/UI/InlineMarkdown.vue'
import { computed, ref } from 'vue'
import apis from '/@/lib/apis'
import type { ClipFolderId } from '/@/types/entity-ids'
import { useMessagesStore } from '/@/store/entities/messages'
import useSortedClipFolders from '/@/composables/clips/useSortedClipFolders'
import { useCreateClip } from '/@/composables/clips/createClip'
import ClipFolderNew from './ClipFolderNew.vue'

const props = defineProps<{
messageId: string
Expand All @@ -85,7 +45,11 @@ apis.getMessageClips(props.messageId).then(res => {
})

const messageContent = computed(() => message.value?.content ?? '')
const { toggleClip } = useCreateClip(props, isSelected)
const { toggleClip } = useCreateClip(props.messageId, isSelected)

const handleCreateClipFolder = (newClipFolderId: ClipFolderId) => {
toggleClip(newClipFolderId)
}
</script>

<style lang="scss" module>
Expand All @@ -105,4 +69,10 @@ const { toggleClip } = useCreateClip(props, isSelected)
margin-bottom: 0;
}
}

.buttonContainer {
display: flex;
justify-content: center;
margin-top: 16px;
}
</style>
102 changes: 102 additions & 0 deletions src/components/Modal/ClipCreateModal/ClipFolderNew.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<template>
<div :class="$style.container">
<a-icon :class="$style.icon" mdi name="bookmark" />
<div :class="$style.inputContainer">
<input
v-model="clipFolderName"
type="text"
:class="$style.input"
placeholder="クリップフォルダを新規作成"
/>
<length-count :val="clipFolderName" :max-length="30" />
</div>
<button
:class="$style.button"
:disabled="clipFolderName.length === 0 || isExceeded || adding"
@click="createClipFolder"
>
<a-icon name="plus" mdi :class="$style.icon" />
</button>
</div>
</template>

<script lang="ts" setup>
import { ref, reactive } from 'vue'
import AIcon from '/@/components/UI/AIcon.vue'
import useMaxLength from '/@/composables/utils/useMaxLength'
import apis from '/@/lib/apis'
import { useToastStore } from '/@/store/ui/toast'
import LengthCount from '/@/components/UI/LengthCount.vue'
import type { ClipFolderId } from '/@/types/entity-ids'

const emit = defineEmits<{
(e: 'createClipFolder', id: ClipFolderId): void
}>()

const { addErrorToast } = useToastStore()

const clipFolderName = ref('')
const adding = ref(false)
const { isExceeded } = useMaxLength(
reactive({ val: clipFolderName, maxLength: 30 })
)

const createClipFolder = async () => {
adding.value = true
try {
const newClipFolder = (await apis.createClipFolder({
name: clipFolderName.value,
description: ''
})).data
clipFolderName.value = ''
emit('createClipFolder', newClipFolder.id)
} catch {
addErrorToast('クリップフォルダの作成に失敗しました')
}
adding.value = false
}
</script>

<style lang="scss" module>
.container {
@include color-ui-primary;
display: flex;
cursor: pointer;
gap: 8px;
padding: 8px 0;
}
.icon {
vertical-align: middle;
}

.inputContainer {
@include color-ui-secondary;
@include background-secondary;
display: flex;
align-items: center;
flex: 1 1;
padding: 4px;
border-radius: 6px;
}

.input {
@include color-text-primary;
width: 100%;
padding: 0 8px;
&::placeholder {
@include color-ui-secondary;
}
}

.button {
@include color-ui-secondary;
@include background-secondary;
padding: 0 12px;
margin-left: 8px;
border-radius: 6px;
cursor: pointer;
&:disabled {
cursor: not-allowed;
}
}
</style>
44 changes: 44 additions & 0 deletions src/composables/clips/createClip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Ref } from 'vue'
import apis from '/@/lib/apis'
import type { MessageId, ClipFolderId } from '/@/types/entity-ids'
import { useToastStore } from '/@/store/ui/toast'
import type { AxiosError } from 'axios'

export const useCreateClip = (
messageId: MessageId,
isSelected: Ref<Set<ClipFolderId>>
) => {
const { addSuccessToast, addErrorToast } = useToastStore()

const createClip = async (clipFolderId: ClipFolderId) => {
try {
await apis.clipMessage(clipFolderId, {
messageId: messageId
})
isSelected.value.add(clipFolderId)
addSuccessToast('クリップフォルダに追加しました')
} catch (e) {
if ((e as AxiosError).response?.status === 409) {
isSelected.value.add(clipFolderId)
addErrorToast('すでに追加されています')
return
} else {
addErrorToast('追加に失敗しました')
}
throw e
}
}
const deleteClip = async (clipFolderId: ClipFolderId) => {
await apis.unclipMessage(clipFolderId, messageId)
isSelected.value.delete(clipFolderId)
addSuccessToast('クリップフォルダから削除しました')
}
const toggleClip = async (clipFolderId: ClipFolderId) => {
if (isSelected.value.has(clipFolderId)) {
await deleteClip(clipFolderId)
} else {
await createClip(clipFolderId)
}
}
return { toggleClip }
}
Loading