admin: streamline item modal actions
This commit is contained in:
@@ -12,6 +12,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const draft = ref('')
|
||||
const isComposing = ref(false)
|
||||
|
||||
const normalizedTags = computed(() =>
|
||||
Array.from(
|
||||
@@ -50,6 +51,7 @@ function removeTag(tag) {
|
||||
}
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.isComposing || isComposing.value) return
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault()
|
||||
addDraftTag()
|
||||
@@ -62,8 +64,17 @@ function handleKeydown(event) {
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
if (isComposing.value) return
|
||||
addDraftTag()
|
||||
}
|
||||
|
||||
function handleCompositionStart() {
|
||||
isComposing.value = true
|
||||
}
|
||||
|
||||
function handleCompositionEnd() {
|
||||
isComposing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -83,6 +94,8 @@ function handleBlur() {
|
||||
:maxlength="maxTagLength"
|
||||
@keydown="handleKeydown"
|
||||
@blur="handleBlur"
|
||||
@compositionstart="handleCompositionStart"
|
||||
@compositionend="handleCompositionEnd"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
export function useAdminCustomItems({
|
||||
api,
|
||||
toast,
|
||||
@@ -26,8 +24,6 @@ export function useAdminCustomItems({
|
||||
selectedTemplateId,
|
||||
refreshCustomItems,
|
||||
loadTemplate,
|
||||
setTab,
|
||||
selectAdminTemplate,
|
||||
resetMessages,
|
||||
success,
|
||||
error,
|
||||
@@ -76,6 +72,7 @@ export function useAdminCustomItems({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
filter: 'all',
|
||||
collapseShared: true,
|
||||
})
|
||||
customItemReplacementItems.value = (data.items || []).filter((item) => item?.id && item.id !== currentItemId)
|
||||
} catch (e) {
|
||||
@@ -138,15 +135,6 @@ export function useAdminCustomItems({
|
||||
customItemDeleteModalOpen.value = false
|
||||
}
|
||||
|
||||
function jumpToTemplateAdmin(templateId) {
|
||||
if (!templateId) return
|
||||
closeCustomItemModal()
|
||||
setTab('template-admin')
|
||||
nextTick(() => {
|
||||
selectAdminTemplate(templateId)
|
||||
})
|
||||
}
|
||||
|
||||
async function removeCustomItem(item = modalTargetCustomItem.value) {
|
||||
resetMessages()
|
||||
if (!item) return
|
||||
@@ -255,6 +243,30 @@ export function useAdminCustomItems({
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkCustomItemTemplate(item = modalTargetCustomItem.value, template) {
|
||||
resetMessages()
|
||||
if (!item?.id || !template?.id) {
|
||||
error.value = '제외할 템플릿 정보를 찾지 못했어요.'
|
||||
return
|
||||
}
|
||||
|
||||
const ok = window.confirm(`"${template.name}" 템플릿에서 이 이미지를 제외할까요?`)
|
||||
if (!ok) return
|
||||
|
||||
try {
|
||||
await api.unlinkAdminCustomItemTemplate(item.id, { topicId: template.id })
|
||||
if (selectedTemplateId.value === template.id) await loadTemplate()
|
||||
await refreshCustomItems()
|
||||
modalTargetCustomItem.value = {
|
||||
...item,
|
||||
linkedTemplates: (item.linkedTemplates || []).filter((entry) => entry.id !== template.id),
|
||||
}
|
||||
success.value = `"${template.name}" 템플릿에서 이미지를 제외했어요.`
|
||||
} catch (e) {
|
||||
error.value = '템플릿 연결 해제에 실패했어요.'
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceCustomItem(item = modalTargetCustomItem.value) {
|
||||
resetMessages()
|
||||
const targetItem = customItemReplacementItems.value.find((entry) => entry.id === customItemReplacementTargetId.value)
|
||||
@@ -315,12 +327,12 @@ export function useAdminCustomItems({
|
||||
closeCustomItemModal,
|
||||
openCustomItemDeleteModal,
|
||||
closeCustomItemDeleteModal,
|
||||
jumpToTemplateAdmin,
|
||||
removeCustomItem,
|
||||
removeUnusedCustomItems,
|
||||
showUnusedCustomItems,
|
||||
saveCustomItemModalLabel,
|
||||
promoteCustomItem,
|
||||
unlinkCustomItemTemplate,
|
||||
refreshReplacementCandidates,
|
||||
replaceCustomItem,
|
||||
restoreCustomItem,
|
||||
|
||||
@@ -77,9 +77,9 @@ export const api = {
|
||||
request(`/api/admin/templates/${encodeURIComponent(templateId)}`, { method: 'PATCH', body: payload }),
|
||||
updateAdminTemplateItem: (templateId, itemId, payload) =>
|
||||
request(`/api/admin/templates/${encodeURIComponent(templateId)}/items/${encodeURIComponent(itemId)}`, { method: 'PATCH', body: payload }),
|
||||
listAdminCustomItems: ({ q = '', page = 1, limit = 50, filter = 'all' } = {}) =>
|
||||
listAdminCustomItems: ({ q = '', page = 1, limit = 50, filter = 'all', collapseShared = false } = {}) =>
|
||||
request(
|
||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}`
|
||||
`/api/admin/custom-items?q=${encodeURIComponent(q)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}&filter=${encodeURIComponent(filter)}&collapseShared=${encodeURIComponent(collapseShared ? '1' : '0')}`
|
||||
),
|
||||
listAdminTierLists: ({ q = '', topicId = '', sort = 'recent', minFavorites = 0, page = 1, limit = 50 } = {}) =>
|
||||
request(`/api/admin/tierlists?q=${encodeURIComponent(q)}&topicId=${encodeURIComponent(topicId)}&sort=${encodeURIComponent(sort)}&minFavorites=${encodeURIComponent(minFavorites)}&page=${encodeURIComponent(page)}&limit=${encodeURIComponent(limit)}`),
|
||||
@@ -104,6 +104,8 @@ export const api = {
|
||||
cleanupAdminUnusedImageAssets: (payload) => request('/api/admin/image-assets/cleanup', { method: 'POST', body: payload || {} }),
|
||||
promoteAdminTemplateItem: (itemId, payload) =>
|
||||
request(`/api/admin/custom-items/${encodeURIComponent(itemId)}/promote`, { method: 'POST', body: payload }),
|
||||
unlinkAdminCustomItemTemplate: (itemId, payload) =>
|
||||
request(`/api/admin/custom-items/${encodeURIComponent(itemId)}/unlink-template`, { method: 'POST', body: payload }),
|
||||
replaceAdminCustomItem: (itemId, payload) =>
|
||||
request(`/api/admin/custom-items/${encodeURIComponent(itemId)}/replace`, { method: 'POST', body: payload }),
|
||||
restoreAdminCustomItem: (itemId) =>
|
||||
|
||||
@@ -891,6 +891,7 @@ async function refreshCustomItems() {
|
||||
page: customItemPage.value,
|
||||
limit: customItemLimit.value,
|
||||
filter: customItemFilter.value,
|
||||
collapseShared: !['user', 'template', 'unused-user', 'replaced-user'].includes(customItemFilter.value),
|
||||
})
|
||||
customItems.value = data.items || []
|
||||
customItemTotal.value = data.total || 0
|
||||
@@ -1096,12 +1097,12 @@ const {
|
||||
closeCustomItemModal,
|
||||
openCustomItemDeleteModal,
|
||||
closeCustomItemDeleteModal,
|
||||
jumpToTemplateAdmin,
|
||||
removeCustomItem,
|
||||
removeUnusedCustomItems,
|
||||
showUnusedCustomItems,
|
||||
saveCustomItemModalLabel,
|
||||
promoteCustomItem,
|
||||
unlinkCustomItemTemplate,
|
||||
refreshReplacementCandidates,
|
||||
replaceCustomItem,
|
||||
restoreCustomItem,
|
||||
@@ -1131,8 +1132,6 @@ const {
|
||||
selectedTemplateId,
|
||||
refreshCustomItems,
|
||||
loadTemplate,
|
||||
setTab,
|
||||
selectAdminTemplate,
|
||||
resetMessages,
|
||||
success,
|
||||
error,
|
||||
@@ -1826,6 +1825,7 @@ async function searchTemplateLibraryItems() {
|
||||
page: 1,
|
||||
limit: 50,
|
||||
filter: 'library',
|
||||
collapseShared: true,
|
||||
})
|
||||
templateLibraryItemResults.value = (data.items || []).filter((item) => item?.id)
|
||||
templateLibraryItemSelectedIds.value = templateLibraryItemSelectedIds.value.filter((id) =>
|
||||
@@ -2377,7 +2377,6 @@ function openUserProfile(user) {
|
||||
</div>
|
||||
<div class="customItemModal__pickerActions">
|
||||
<button class="btn btn--ghost" type="button" @click="openTemplatePickerModal('custom-item-target')">템플릿 선택</button>
|
||||
<button class="btn btn--ghost btn--small customItemModal__createTemplateButton" type="button" @click="openTemplateCreateModal">새 템플릿 만들기</button>
|
||||
</div>
|
||||
<template v-if="canReplaceModalTarget">
|
||||
<div class="customItemModal__pickerHead">
|
||||
@@ -2463,7 +2462,10 @@ function openUserProfile(user) {
|
||||
<div class="customItemModal__linked">
|
||||
<span class="customItemModal__label">이 이미지를 사용하는 템플릿</span>
|
||||
<div v-if="visibleLinkedTemplates.length" class="customItemModal__chips">
|
||||
<button v-for="template in visibleLinkedTemplates" :key="template.id" type="button" class="pill pill--link" @click="jumpToTemplateAdmin(template.id)">{{ template.name }}</button>
|
||||
<span v-for="template in visibleLinkedTemplates" :key="template.id" class="customItemModal__templateChip">
|
||||
<span>{{ template.name }}</span>
|
||||
<button class="customItemModal__templateChipRemove" type="button" @click="unlinkCustomItemTemplate(modalTargetCustomItem, template)">X</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="hint hint--tight">아직 템플릿에 연결된 항목이 없어요.</div>
|
||||
</div>
|
||||
@@ -4034,9 +4036,6 @@ function openUserProfile(user) {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
.adminUiScope .customItemModal__createTemplateButton {
|
||||
justify-self: start;
|
||||
}
|
||||
.adminUiScope .customItemModal__body {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
@@ -4121,6 +4120,26 @@ function openUserProfile(user) {
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.adminUiScope .customItemModal__templateChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 11px;
|
||||
border-radius: 999px;
|
||||
background: var(--theme-pill-bg);
|
||||
border: 1px solid var(--theme-border);
|
||||
font-size: 12px;
|
||||
color: var(--theme-text);
|
||||
}
|
||||
.adminUiScope .customItemModal__templateChipRemove {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--theme-text-soft);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.adminUiScope .customItemModal__title {
|
||||
font-size: 19px;
|
||||
font-weight: 900;
|
||||
|
||||
Reference in New Issue
Block a user