diff --git a/locales/index.d.ts b/locales/index.d.ts index 83159337ae1c4bd85a6c7d99c2c4185d9e0d8c4d..a0540fd22883004449853ed0452c0062e2d05655 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -10588,7 +10588,7 @@ export interface Locale extends ILocale { */ "deleteSelectionRows": string; /** - * é¸æŠžç¯„囲ã®è¡Œã‚’削除 + * é¸æŠžç¯„囲ã®å€¤ã‚’クリア */ "deleteSelectionRanges": string; /** @@ -10599,6 +10599,10 @@ export interface Locale extends ILocale { * 検索æ¡ä»¶ã‚’詳細ã«è¨å®šã—ã¾ã™ã€‚ */ "searchSettingCaption": string; + /** + * 表示件数 + */ + "searchLimit": string; /** * 並ã³é † */ @@ -10611,10 +10615,6 @@ export interface Locale extends ILocale { * 絵文å—更新・削除時ã®ãƒã‚°ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚更新・削除æ“作を行ã£ãŸã‚Šã€ãƒšãƒ¼ã‚¸ã‚’é·ç§»ãƒ»ãƒªãƒãƒ¼ãƒ‰ã™ã‚‹ã¨æ¶ˆãˆã¾ã™ã€‚ */ "registrationLogsCaption": string; - /** - * エラー - */ - "alertEmojisRegisterFailedTitle": string; /** * 絵文å—ã®æ›´æ–°ãƒ»å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸã€‚詳細ã¯ç™»éŒ²ãƒã‚°ã‚’ã”確èªãã ã•ã„。 */ @@ -10691,21 +10691,30 @@ export interface Locale extends ILocale { */ "alertDeleteEmojisNothingDescription": string; /** - * ç¢ºèª + * ページを移動ã—ã¾ã™ã‹ï¼Ÿ */ - "confirmUpdateEmojisTitle": string; + "confirmMovePage": string; /** - * {count}個ã®çµµæ–‡å—ã‚’æ›´æ–°ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ + * 表示を変更ã—ã¾ã™ã‹ï¼Ÿ */ - "confirmUpdateEmojisDescription": ParameterizedString<"count">; + "confirmChangeView": string; /** - * ç¢ºèª + * {count}個ã®çµµæ–‡å—ã‚’æ›´æ–°ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ */ - "confirmDeleteEmojisTitle": string; + "confirmUpdateEmojisDescription": ParameterizedString<"count">; /** * ãƒã‚§ãƒƒã‚¯ãŒã¤ã‘られãŸ{count}個ã®çµµæ–‡å—を削除ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ */ "confirmDeleteEmojisDescription": ParameterizedString<"count">; + /** + * 今ã¾ã§ã«åŠ ãˆãŸå¤‰æ›´ãŒã™ã¹ã¦ãƒªã‚»ãƒƒãƒˆã•ã‚Œã¾ã™ã€‚ + */ + "confirmResetDescription": string; + /** + * ã“ã®ãƒšãƒ¼ã‚¸ã®çµµæ–‡å—ã«å¤‰æ›´ãŒåŠ ãˆã‚‰ã‚Œã¦ã„ã¾ã™ã€‚ + * ä¿å˜ã›ãšã«ã“ã®ã¾ã¾ãƒšãƒ¼ã‚¸ã‚’移動ã™ã‚‹ã¨ã€ã“ã®ãƒšãƒ¼ã‚¸ã§åŠ ãˆãŸå¤‰æ›´ã¯ã™ã¹ã¦ç ´æ£„ã•ã‚Œã¾ã™ã€‚ + */ + "confirmMovePageDesciption": string; /** * 絵文å—ã«è¨å®šã•ã‚ŒãŸãƒãƒ¼ãƒ«ã§æ¤œç´¢ */ @@ -10744,26 +10753,14 @@ export interface Locale extends ILocale { * ã“ã®ãƒªãƒ³ã‚¯ã‚’クリックã—ã¦ãƒ‰ãƒ©ã‚¤ãƒ–ã‹ã‚‰é¸æŠžã™ã‚‹ */ "emojiInputAreaList3": string; - /** - * ç¢ºèª - */ - "confirmRegisterEmojisTitle": string; /** * リストã«è¡¨ç¤ºã•ã‚Œã¦ã„る絵文å—ã‚’æ–°ãŸãªã‚«ã‚¹ã‚¿ãƒ 絵文å—ã¨ã—ã¦ç™»éŒ²ã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿï¼ˆè² è·ã‚’é¿ã‘ã‚‹ãŸã‚ã€ä¸€åº¦ã®æ“作ã§ç™»éŒ²å¯èƒ½ãªçµµæ–‡å—ã¯{count}件ã¾ã§ã§ã™ï¼‰ */ "confirmRegisterEmojisDescription": ParameterizedString<"count">; - /** - * ç¢ºèª - */ - "confirmClearEmojisTitle": string; /** * ç·¨é›†å†…å®¹ã‚’ç ´æ£„ã—ã€ãƒªã‚¹ãƒˆã«è¡¨ç¤ºã•ã‚Œã¦ã„る絵文å—をクリアã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ */ "confirmClearEmojisDescription": string; - /** - * ç¢ºèª - */ - "confirmUploadEmojisTitle": string; /** * ドラッグ&ドãƒãƒƒãƒ—ã•ã‚ŒãŸ{count}個ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドライブã«ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 42c25a58a618a1c7ca945a09671377b84fbd455b..a578704434eafb0a61555265bf74aa9cd62755e1 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2824,13 +2824,13 @@ _customEmojisManager: copySelectionRows: "é¸æŠžè¡Œã‚’コピー" copySelectionRanges: "é¸æŠžç¯„囲をコピー" deleteSelectionRows: "é¸æŠžè¡Œã‚’削除" - deleteSelectionRanges: "é¸æŠžç¯„囲ã®è¡Œã‚’削除" + deleteSelectionRanges: "é¸æŠžç¯„囲ã®å€¤ã‚’クリア" searchSettings: "検索è¨å®š" searchSettingCaption: "検索æ¡ä»¶ã‚’詳細ã«è¨å®šã—ã¾ã™ã€‚" + searchLimit: "表示件数" sortOrder: "並ã³é †" registrationLogs: "登録ãƒã‚°" registrationLogsCaption: "絵文å—更新・削除時ã®ãƒã‚°ãŒè¡¨ç¤ºã•ã‚Œã¾ã™ã€‚更新・削除æ“作を行ã£ãŸã‚Šã€ãƒšãƒ¼ã‚¸ã‚’é·ç§»ãƒ»ãƒªãƒãƒ¼ãƒ‰ã™ã‚‹ã¨æ¶ˆãˆã¾ã™ã€‚" - alertEmojisRegisterFailedTitle: "エラー" alertEmojisRegisterFailedDescription: "絵文å—ã®æ›´æ–°ãƒ»å‰Šé™¤ã«å¤±æ•—ã—ã¾ã—ãŸã€‚詳細ã¯ç™»éŒ²ãƒã‚°ã‚’ã”確èªãã ã•ã„。" _logs: showSuccessLogSwitch: "æˆåŠŸãƒã‚°ã‚’表示" @@ -2852,10 +2852,12 @@ _customEmojisManager: markAsDeleteTargetRanges: "é¸æŠžç¯„囲ã®è¡Œã‚’削除対象ã«ã™ã‚‹" alertUpdateEmojisNothingDescription: "変更ã•ã‚ŒãŸçµµæ–‡å—ã¯ã‚ã‚Šã¾ã›ã‚“。" alertDeleteEmojisNothingDescription: "削除対象ã®çµµæ–‡å—ã¯ã‚ã‚Šã¾ã›ã‚“。" - confirmUpdateEmojisTitle: "確èª" + confirmMovePage: "ページを移動ã—ã¾ã™ã‹ï¼Ÿ" + confirmChangeView: "表示を変更ã—ã¾ã™ã‹ï¼Ÿ" confirmUpdateEmojisDescription: "{count}個ã®çµµæ–‡å—ã‚’æ›´æ–°ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ" - confirmDeleteEmojisTitle: "確èª" confirmDeleteEmojisDescription: "ãƒã‚§ãƒƒã‚¯ãŒã¤ã‘られãŸ{count}個ã®çµµæ–‡å—を削除ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ" + confirmResetDescription: "今ã¾ã§ã«åŠ ãˆãŸå¤‰æ›´ãŒã™ã¹ã¦ãƒªã‚»ãƒƒãƒˆã•ã‚Œã¾ã™ã€‚" + confirmMovePageDesciption: "ã“ã®ãƒšãƒ¼ã‚¸ã®çµµæ–‡å—ã«å¤‰æ›´ãŒåŠ ãˆã‚‰ã‚Œã¦ã„ã¾ã™ã€‚\nä¿å˜ã›ãšã«ã“ã®ã¾ã¾ãƒšãƒ¼ã‚¸ã‚’移動ã™ã‚‹ã¨ã€ã“ã®ãƒšãƒ¼ã‚¸ã§åŠ ãˆãŸå¤‰æ›´ã¯ã™ã¹ã¦ç ´æ£„ã•ã‚Œã¾ã™ã€‚" dialogSelectRoleTitle: "絵文å—ã«è¨å®šã•ã‚ŒãŸãƒãƒ¼ãƒ«ã§æ¤œç´¢" _register: uploadSettingTitle: "アップãƒãƒ¼ãƒ‰è¨å®š" @@ -2866,11 +2868,8 @@ _customEmojisManager: emojiInputAreaList1: "ã“ã®æž ã«ç”»åƒãƒ•ã‚¡ã‚¤ãƒ«ã¾ãŸã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚’ドラッグ&ドãƒãƒƒãƒ—" emojiInputAreaList2: "ã“ã®ãƒªãƒ³ã‚¯ã‚’クリックã—ã¦PCã‹ã‚‰é¸æŠžã™ã‚‹" emojiInputAreaList3: "ã“ã®ãƒªãƒ³ã‚¯ã‚’クリックã—ã¦ãƒ‰ãƒ©ã‚¤ãƒ–ã‹ã‚‰é¸æŠžã™ã‚‹" - confirmRegisterEmojisTitle: "確èª" confirmRegisterEmojisDescription: "リストã«è¡¨ç¤ºã•ã‚Œã¦ã„る絵文å—ã‚’æ–°ãŸãªã‚«ã‚¹ã‚¿ãƒ 絵文å—ã¨ã—ã¦ç™»éŒ²ã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿï¼ˆè² è·ã‚’é¿ã‘ã‚‹ãŸã‚ã€ä¸€åº¦ã®æ“作ã§ç™»éŒ²å¯èƒ½ãªçµµæ–‡å—ã¯{count}件ã¾ã§ã§ã™ï¼‰" - confirmClearEmojisTitle: "確èª" confirmClearEmojisDescription: "ç·¨é›†å†…å®¹ã‚’ç ´æ£„ã—ã€ãƒªã‚¹ãƒˆã«è¡¨ç¤ºã•ã‚Œã¦ã„る絵文å—をクリアã—ã¾ã™ã€‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ" - confirmUploadEmojisTitle: "確èª" confirmUploadEmojisDescription: "ドラッグ&ドãƒãƒƒãƒ—ã•ã‚ŒãŸ{count}個ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚’ドライブã«ã‚¢ãƒƒãƒ—ãƒãƒ¼ãƒ‰ã—ã¾ã™ã€‚実行ã—ã¾ã™ã‹ï¼Ÿ" _embedCodeGen: diff --git a/packages/frontend/src/components/MkSortOrderEditor.vue b/packages/frontend/src/components/MkSortOrderEditor.vue index da08f1229716905b2a3a8321662d59551467b730..9decacc5f56ec8715d31d15f1ad803f07bdee1aa 100644 --- a/packages/frontend/src/components/MkSortOrderEditor.vue +++ b/packages/frontend/src/components/MkSortOrderEditor.vue @@ -12,12 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only :iconClass="order.direction === '+' ? 'ti ti-arrow-up' : 'ti ti-arrow-down'" :exButtonIconClass="'ti ti-x'" :content="order.key" + :class="$style.sortOrderTag" @click="onToggleSortOrderButtonClicked(order)" @exButtonClick="onRemoveSortOrderButtonClicked(order)" /> </div> <MkButton :class="$style.sortOrderAddButton" @click="onAddSortOrderButtonClicked"> - <span class="ti ti-plus"/> + <span class="ti ti-plus"></span> </MkButton> </div> </template> @@ -109,4 +110,9 @@ function emitOrder(sortOrders: SortOrder<T>[]) { border-radius: 9999px; background-color: var(--MI_THEME-buttonBg); } + +.sortOrderTag { + user-select: none; + cursor: pointer; +} </style> diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 0caaed6f39d0cf820b21f6ebaf168bd2181cd327..fa0e40d8f960656198eda9164bba912237978bbf 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -47,7 +47,7 @@ export type SuperMenuDef = { active?: boolean; action: (ev: MouseEvent) => void; } | { - type: 'link'; + type?: 'link'; to: string; icon?: string; text: string; diff --git a/packages/frontend/src/components/MkTagItem.vue b/packages/frontend/src/components/MkTagItem.vue index 98f2411392952f3cc96f301403a6117d1ef95fea..8b7460f3a35c3e0f6935f8b9848b3ac5815731f5 100644 --- a/packages/frontend/src/components/MkTagItem.vue +++ b/packages/frontend/src/components/MkTagItem.vue @@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div :class="$style.root" @click="(ev) => emit('click', ev)"> - <span v-if="iconClass" :class="[$style.icon, iconClass]"/> + <span v-if="iconClass" :class="[$style.icon, iconClass]"></span> <span :class="$style.content">{{ content }}</span> <MkButton v-if="exButtonIconClass" :class="$style.exButton" @click="(ev) => emit('exButtonClick', ev)"> - <span :class="[$style.exButtonIcon, exButtonIconClass]"/> + <span :class="[$style.exButtonIcon, exButtonIconClass]"></span> </MkButton> </div> </template> diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index aa4be69b2cbf7d2af62c8b97425ccf151eabeb49..a2e70a5cad6720c0a65f64f27b4284da987d42eb 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -48,13 +48,16 @@ import { scrollToTop } from '@@/js/scroll.js'; import { globalEvents } from '@/events.js'; import { injectReactiveMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; -import { PageHeaderItem } from '@/types/page-header.js'; +import type { PageHeaderItem } from '@/types/page-header.js'; +import type { PageMetadata } from '@/scripts/page-metadata.js'; const props = withDefaults(defineProps<{ + overridePageMetadata?: PageMetadata; tabs?: Tab[]; tab?: string; actions?: PageHeaderItem[] | null; thin?: boolean; + hideTitle?: boolean; displayMyAvatar?: boolean; }>(), { tabs: () => ([] as Tab[]), @@ -64,9 +67,10 @@ const emit = defineEmits<{ (ev: 'update:tab', key: string); }>(); -const pageMetadata = injectReactiveMetadata(); +const injectedPageMetadata = injectReactiveMetadata(); +const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value); -const hideTitle = inject('shouldOmitHeaderTitle', false); +const hideTitle = computed(() => inject('shouldOmitHeaderTitle', false) || props.hideTitle); const thin_ = props.thin || inject('shouldHeaderThin', false); const el = shallowRef<HTMLElement | undefined>(undefined); @@ -75,7 +79,7 @@ const narrow = ref(false); const hasTabs = computed(() => props.tabs.length > 0); const hasActions = computed(() => props.actions && props.actions.length > 0); const show = computed(() => { - return !hideTitle || hasTabs.value || hasActions.value; + return !hideTitle.value || hasTabs.value || hasActions.value; }); const preventDrag = (ev: TouchEvent) => { diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue index 0ffd42abda3050157fed566075fb27953186555c..e473b7c1afef531bfcd8fed68661eb86f1a0fa3a 100644 --- a/packages/frontend/src/components/grid/MkDataCell.vue +++ b/packages/frontend/src/components/grid/MkDataCell.vue @@ -39,13 +39,15 @@ SPDX-License-Identifier: AGPL-3.0-only {{ cell.value }} </div> <div v-else-if="cellType === 'boolean'"> - <span v-if="cell.value === true" class="ti ti-check"/> - <span v-else class="ti"/> + <div :class="[$style.bool, { + [$style.boolTrue]: cell.value === true, + 'ti ti-check': cell.value === true, + }]"></div> </div> <div v-else-if="cellType === 'image'"> <img - :src="cell.value as string" - :alt="cell.value as string" + :src="cell.value" + :alt="cell.value" :class="$style.viewImage" @load="emitContentSizeChanged" /> @@ -375,6 +377,31 @@ $cellHeight: 28px; object-fit: cover; } +.bool { + position: relative; + width: 18px; + height: 18px; + background: var(--MI_THEME-panel); + border: solid 2px var(--MI_THEME-divider); + border-radius: 4px; + box-sizing: border-box; + + &.boolTrue { + border-color: var(--MI_THEME-accent); + background: var(--MI_THEME-accent); + + &::before { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--MI_THEME-fgOnAccent); + font-size: 12px; + line-height: 18px; + } + } +} + .editingInput { padding: 0 8px; width: 100%; diff --git a/packages/frontend/src/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue index 60738365fb6da84dbc860989825572519709a8a7..4dbd4ebcae133edb9f39242e774c52b22cb3006f 100644 --- a/packages/frontend/src/components/grid/MkGrid.vue +++ b/packages/frontend/src/components/grid/MkGrid.vue @@ -7,7 +7,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div ref="rootEl" class="mk_grid_border" - :class="[$style.grid]" + :class="[$style.grid, { + [$style.noOverflowHandling]: rootSetting.noOverflowStyle, + 'mk_grid_root_rounded': rootSetting.rounded, + 'mk_grid_root_border': rootSetting.outerBorder, + }]" @mousedown.prevent="onMouseDown" @keydown="onKeyDown" @contextmenu.prevent.stop="onContextMenu" @@ -77,10 +81,17 @@ const emit = defineEmits<{ }>(); const props = defineProps<{ - settings: GridSetting, - data: DataSource[] + settings: GridSetting; + data: DataSource[]; }>(); +const rootSetting: Required<GridSetting['root']> = { + noOverflowStyle: false, + rounded: true, + outerBorder: true, + ...props.settings.root, +}; + // non-reactive // eslint-disable-next-line vue/no-setup-props-reactivity-loss const rowSetting: Required<GridRowSetting> = { @@ -1277,32 +1288,48 @@ onMounted(() => { overflow-x: scroll; // firefoxã ã¨ã‚¹ã‚¯ãƒãƒ¼ãƒ«ãƒãƒ¼ãŒã‚»ãƒ«ã«é‡ãªã£ã¦è¦‹ã¥ã‚‰ããªã£ã¦ã—ã¾ã†ã®ã§ã‚¹ãƒšãƒ¼ã‚¹ã‚’空ã‘ã¦ãŠã padding-bottom: 8px; + + &.noOverflowHandling { + overflow-x: revert; + padding-bottom: 0; + } } </style> <style lang="scss"> $borderSetting: solid 0.5px var(--MI_THEME-divider); -$borderRadius: var(--MI-radius); // é…下コンãƒãƒ¼ãƒãƒ³ãƒˆã‚’å«ã‚ã¦ä¸€æ‹¬ã—ã¦ã‚³ãƒ³ãƒˆãƒãƒ¼ãƒ«ã™ã‚‹ãŸã‚ã€scopedã‚‚moduleも使用ã§ããªã„ .mk_grid_border { + --rootBorderSetting: none; + --borderRadius: 0; + border-spacing: 0; + &.mk_grid_root_border { + --rootBorderSetting: #{$borderSetting}; + } + + &.mk_grid_root_rounded { + --borderRadius: var(--MI-radius); + } + .mk_grid_thead { .mk_grid_tr { .mk_grid_th { border-left: $borderSetting; - border-top: $borderSetting; + border-top: var(--rootBorderSetting); &:first-child { // 左上セル - border-top-left-radius: $borderRadius; + border-left: var(--rootBorderSetting); + border-top-left-radius: var(--borderRadius); } &:last-child { // å³ä¸Šã‚»ãƒ« - border-top-right-radius: $borderRadius; - border-right: $borderSetting; + border-top-right-radius: var(--borderRadius); + border-right: var(--rootBorderSetting); } } } @@ -1314,9 +1341,14 @@ $borderRadius: var(--MI-radius); border-left: $borderSetting; border-top: $borderSetting; + &:first-child { + // 左端ã®åˆ— + border-left: var(--rootBorderSetting); + } + &:last-child { // 一番å³ç«¯ã®åˆ— - border-right: $borderSetting; + border-right: var(--rootBorderSetting); } } } @@ -1324,16 +1356,16 @@ $borderRadius: var(--MI-radius); .last_row { .mk_grid_td, .mk_grid_th { // 一番下ã®è¡Œ - border-bottom: $borderSetting; + border-bottom: var(--rootBorderSetting); &:first-child { // 左下セル - border-bottom-left-radius: $borderRadius; + border-bottom-left-radius: var(--borderRadius); } &:last-child { // å³ä¸‹ã‚»ãƒ« - border-bottom-right-radius: $borderRadius; + border-bottom-right-radius: var(--borderRadius); } } } diff --git a/packages/frontend/src/components/grid/MkHeaderCell.vue b/packages/frontend/src/components/grid/MkHeaderCell.vue index 605d27c6d61a1c30f1d19ba1f886ab55ee650ce3..aecfe7eaa3b8bf654b6468802303f5775e38f176 100644 --- a/packages/frontend/src/components/grid/MkHeaderCell.vue +++ b/packages/frontend/src/components/grid/MkHeaderCell.vue @@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only :data-grid-cell-col="column.index" > <div :class="$style.root"> - <div :class="$style.left"/> + <div :class="$style.left"></div> <div :class="$style.wrapper"> <div ref="contentEl" :class="$style.contentArea"> - <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"/> + <span v-if="column.setting.icon" class="ti" :class="column.setting.icon" style="line-height: normal"></span> <span v-else>{{ text }}</span> </div> </div> @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only :class="$style.right" @mousedown="onHandleMouseDown" @dblclick="onHandleDoubleClick" - /> + ></div> </div> </div> </template> diff --git a/packages/frontend/src/components/grid/grid.ts b/packages/frontend/src/components/grid/grid.ts index 0cb3b6f28b44424939fca5e452dc23b45605e295..b82e12b3045f7193b4c3bb67f49d20af70e79093 100644 --- a/packages/frontend/src/components/grid/grid.ts +++ b/packages/frontend/src/components/grid/grid.ts @@ -9,6 +9,11 @@ import { GridColumnSetting } from '@/components/grid/column.js'; import { GridRowSetting } from '@/components/grid/row.js'; export type GridSetting = { + root?: { + noOverflowStyle?: boolean; + rounded?: boolean; + outerBorder?: boolean; + }; row?: GridRowSetting; cols: GridColumnSetting[]; cells?: GridCellSetting; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts index de2b2aca8c625abe5ca177ce3c642b02f2ec16bc..141ab858d31e1b500b5d2bcefd028022570475ac 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.impl.ts @@ -22,7 +22,8 @@ export const gridSortOrderKeys = [ 'isSensitive', 'localOnly', 'updatedAt', -]; +] as const satisfies string[]; + export type GridSortOrderKey = typeof gridSortOrderKeys[number]; export function emptyStrToUndefined(value: string | null) { diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue new file mode 100644 index 0000000000000000000000000000000000000000..4b145db0edf551f3516240f7f935e77383f1b58c --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.logs.vue @@ -0,0 +1,39 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="uiWindow" + :initialWidth="400" + :initialHeight="500" + :canResize="true" + @closed="emit('closed')" +> + <template #header> + <i class="ti ti-notes" style="margin-right: 0.5em;"></i> {{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }} + </template> + <MkSpacer> + <XRegisterLogs :logs="logs"/> + </MkSpacer> +</MkWindow> +</template> + +<script setup lang="ts"> +import MkWindow from '@/components/MkWindow.vue'; +import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; + +import { i18n } from '@/i18n.js'; + +import type { RequestLogItem } from './custom-emojis-manager.impl.js'; + +defineProps<{ + logs: RequestLogItem[]; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); + +</script> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae43507d665f7928ea3a93ec7b9e7c80d5216797 --- /dev/null +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.search.vue @@ -0,0 +1,213 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkWindow + ref="uiWindow" + :initialWidth="400" + :initialHeight="500" + :canResize="true" + @closed="emit('closed')" +> + <template #header> + <i class="ti ti-search" style="margin-right: 0.5em;"></i> {{ i18n.ts.search }} + </template> + <div :class="$style.root"> + <MkSpacer> + <div class="_gaps"> + <div class="_gaps_s"> + <MkInput + v-model="model.name" + type="search" + autocapitalize="off" + > + <template #label>name</template> + </MkInput> + <MkInput + v-model="model.category" + type="search" + autocapitalize="off" + > + <template #label>category</template> + </MkInput> + <MkInput + v-model="model.aliases" + type="search" + autocapitalize="off" + > + <template #label>aliases</template> + </MkInput> + + <MkInput + v-model="model.type" + type="search" + autocapitalize="off" + > + <template #label>type</template> + </MkInput> + <MkInput + v-model="model.license" + type="search" + autocapitalize="off" + > + <template #label>license</template> + </MkInput> + <MkSelect + v-model="model.sensitive" + > + <template #label>sensitive</template> + <option :value="null">-</option> + <option :value="true">true</option> + <option :value="false">false</option> + </MkSelect> + + <MkSelect + v-model="model.localOnly" + > + <template #label>localOnly</template> + <option :value="null">-</option> + <option :value="true">true</option> + <option :value="false">false</option> + </MkSelect> + <MkInput + v-model="model.updatedAtFrom" + type="date" + autocapitalize="off" + > + <template #label>updatedAt(from)</template> + </MkInput> + <MkInput + v-model="model.updatedAtTo" + type="date" + autocapitalize="off" + > + <template #label>updatedAt(to)</template> + </MkInput> + + <MkInput + v-model="queryRolesText" + type="text" + readonly + autocapitalize="off" + @click="onQueryRolesEditClicked" + > + <template #label>role</template> + <template #suffix><i class="ti ti-pencil"></i></template> + </MkInput> + </div> + <MkFolder :spacerMax="8" :spacerMin="8"> + <template #icon><i class="ti ti-arrows-sort"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> + <MkSortOrderEditor + :baseOrderKeyNames="gridSortOrderKeys" + :currentOrders="sortOrders" + @update="onSortOrderUpdate" + /> + </MkFolder> + </div> + </MkSpacer> + <div :class="$style.footerActions"> + <MkButton primary @click="onSearchRequest"> + {{ i18n.ts.search }} + </MkButton> + <MkButton @click="onQueryResetButtonClicked"> + {{ i18n.ts.reset }} + </MkButton> + </div> + </div> +</MkWindow> +</template> + +<script setup lang="ts"> +import { computed, ref, watch } from 'vue'; +import MkWindow from '@/components/MkWindow.vue'; +import MkInput from '@/components/MkInput.vue'; +import MkSelect from '@/components/MkSelect.vue'; +import MkButton from '@/components/MkButton.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; + +import { + gridSortOrderKeys, +} from './custom-emojis-manager.impl.js'; + +import { i18n } from '@/i18n.js'; +import * as os from '@/os.js'; + +import type { EmojiSearchQuery } from './custom-emojis-manager.local.list.vue'; +import type { SortOrder } from '@/components/MkSortOrderEditor.define.js'; +import type { GridSortOrderKey } from './custom-emojis-manager.impl.js'; + +const props = defineProps<{ + query: EmojiSearchQuery; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; + (ev: 'queryUpdated', query: EmojiSearchQuery): void; + (ev: 'sortOrderUpdated', orders: SortOrder<GridSortOrderKey>[]): void; + (ev: 'search'): void; +}>(); + +const model = ref<EmojiSearchQuery>(props.query); +const queryRolesText = computed(() => model.value.roles.map(it => it.name).join(',')); + +watch(model, () => { + emit('queryUpdated', model.value); +}, { deep: true }); + +const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]); + +function onSortOrderUpdate(orders: SortOrder<GridSortOrderKey>[]) { + sortOrders.value = orders; + emit('sortOrderUpdated', orders); +} + +function onSearchRequest() { + emit('search'); +} + +function onQueryResetButtonClicked() { + model.value.name = ''; + model.value.category = ''; + model.value.aliases = ''; + model.value.type = ''; + model.value.license = ''; + model.value.sensitive = null; + model.value.localOnly = null; + model.value.updatedAtFrom = ''; + model.value.updatedAtTo = ''; + sortOrders.value = []; +} + +async function onQueryRolesEditClicked() { + const result = await os.selectRole({ + initialRoleIds: model.value.roles.map(it => it.id), + title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle, + publicOnly: true, + }); + if (result.canceled) { + return; + } + + model.value.roles = result.result; +} +</script> + +<style module> +.root { + position: relative; +} + +.footerActions { + position: sticky; + bottom: 0; + padding: var(--MI-margin); + background-color: var(--MI_THEME-bg); + display: flex; + gap: 8px; + z-index: 1; +} +</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index 55f9632ce48e35df072b9b77139425932ebc3a9d..c4ea3b93e32aadd6cf9efc46e1e6eaa7a1a8153f 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -5,137 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkStickyContainer> + <template #header> + <MkPageHeader :overridePageMetadata="headerPageMetadata" :actions="headerActions"/> + </template> <template #default> - <div class="_gaps"> - <MkFolder> - <template #icon><i class="ti ti-search"></i></template> - <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchSettings }}</template> - <template #caption> - {{ i18n.ts._customEmojisManager._gridCommon.searchSettingCaption }} - </template> - - <div class="_gaps"> - <div :class="[[spMode ? $style.searchAreaSp : $style.searchArea]]"> - <MkInput - v-model="queryName" - type="search" - autocapitalize="off" - :class="[$style.col1, $style.row1]" - @enter="onSearchRequest" - > - <template #label>name</template> - </MkInput> - <MkInput - v-model="queryCategory" - type="search" - autocapitalize="off" - :class="[$style.col2, $style.row1]" - @enter="onSearchRequest" - > - <template #label>category</template> - </MkInput> - <MkInput - v-model="queryAliases" - type="search" - autocapitalize="off" - :class="[$style.col3, $style.row1]" - @enter="onSearchRequest" - > - <template #label>aliases</template> - </MkInput> - - <MkInput - v-model="queryType" - type="search" - autocapitalize="off" - :class="[$style.col1, $style.row2]" - @enter="onSearchRequest" - > - <template #label>type</template> - </MkInput> - <MkInput - v-model="queryLicense" - type="search" - autocapitalize="off" - :class="[$style.col2, $style.row2]" - @enter="onSearchRequest" - > - <template #label>license</template> - </MkInput> - <MkSelect - v-model="querySensitive" - :class="[$style.col3, $style.row2]" - > - <template #label>sensitive</template> - <option :value="null">-</option> - <option :value="true">true</option> - <option :value="false">false</option> - </MkSelect> - - <MkSelect - v-model="queryLocalOnly" - :class="[$style.col1, $style.row3]" - > - <template #label>localOnly</template> - <option :value="null">-</option> - <option :value="true">true</option> - <option :value="false">false</option> - </MkSelect> - <MkInput - v-model="queryUpdatedAtFrom" - type="date" - autocapitalize="off" - :class="[$style.col2, $style.row3]" - @enter="onSearchRequest" - > - <template #label>updatedAt(from)</template> - </MkInput> - <MkInput - v-model="queryUpdatedAtTo" - type="date" - autocapitalize="off" - :class="[$style.col3, $style.row3]" - @enter="onSearchRequest" - > - <template #label>updatedAt(to)</template> - </MkInput> - - <MkInput - v-model="queryRolesText" - type="text" - readonly - autocapitalize="off" - :class="[$style.col1, $style.row4]" - @click="onQueryRolesEditClicked" - > - <template #label>role</template> - <template #suffix><span class="ti ti-pencil"/></template> - </MkInput> - </div> - - <MkFolder :spacerMax="8" :spacerMin="8"> - <template #icon><i class="ti ti-arrows-sort"></i></template> - <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> - <MkSortOrderEditor - :baseOrderKeyNames="gridSortOrderKeys" - :currentOrders="sortOrders" - @update="onSortOrderUpdate" - /> - </MkFolder> - - <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]"> - <MkButton primary @click="onSearchRequest"> - {{ i18n.ts.search }} - </MkButton> - <MkButton @click="onQueryResetButtonClicked"> - {{ i18n.ts.reset }} - </MkButton> - </div> - </div> - </MkFolder> - - <XRegisterLogsFolder :logs="requestLogs"/> - + <div class="_gaps" :class="$style.main"> <component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/> <template v-else> <div v-if="gridItems.length === 0" style="text-align: center"> @@ -143,65 +17,78 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <template v-else> - <div :class="$style.gridArea"> + <div :class="$style.grid"> <MkGrid :data="gridItems" :settings="setupGrid()" @event="onGridEvent"/> </div> - - <div :class="$style.footer"> - <div :class="$style.left"> - <MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked"> - {{ i18n.ts.delete }} ({{ deleteItemsCount }}) - </MkButton> - </div> - - <div :class="$style.center"> - <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/> - </div> - - <div :class="$style.right"> - <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked"> - {{ i18n.ts.update }} ({{ updatedItemsCount }}) - </MkButton> - <MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton> - </div> - </div> </template> </template> </div> </template> + + <template #footer> + <div v-if="gridItems.length > 0" :class="$style.footer"> + <div :class="$style.left"> + <MkButton danger style="margin-right: auto" @click="onDeleteButtonClicked"> + {{ i18n.ts.delete }} ({{ deleteItemsCount }}) + </MkButton> + </div> + + <div :class="$style.center"> + <MkPagingButtons :current="currentPage" :max="allPages" :buttonCount="5" @pageChanged="onPageChanged"/> + </div> + + <div :class="$style.right"> + <MkButton primary :disabled="updateButtonDisabled" @click="onUpdateButtonClicked"> + {{ i18n.ts.update }} ({{ updatedItemsCount }}) + </MkButton> + <MkButton @click="onGridResetButtonClicked">{{ i18n.ts.reset }}</MkButton> + </div> + </div> + </template> </MkStickyContainer> </template> +<script lang="ts"> +import type { SortOrder } from '@/components/MkSortOrderEditor.define.js'; +import type { GridSortOrderKey } from './custom-emojis-manager.impl.js'; + +export type EmojiSearchQuery = { + name: string | null; + category: string | null; + aliases: string | null; + type: string | null; + license: string | null; + updatedAtFrom: string | null; + updatedAtTo: string | null; + sensitive: string | null; + localOnly: string | null; + roles: { id: string, name: string }[]; + sortOrders: SortOrder<GridSortOrderKey>[]; + limit: number; +}; +</script> + <script setup lang="ts"> -import { computed, onMounted, ref, useCssModule } from 'vue'; +import { computed, defineAsyncComponent, onMounted, ref, nextTick, useCssModule } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { emptyStrToEmptyArray, emptyStrToNull, emptyStrToUndefined, - GridSortOrderKey, - gridSortOrderKeys, RequestLogItem, roleIdsParser, } from '@/pages/admin/custom-emojis-manager.impl.js'; import MkGrid from '@/components/grid/MkGrid.vue'; import { i18n } from '@/i18n.js'; -import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import { validators } from '@/components/grid/cell-validators.js'; import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import MkPagingButtons from '@/components/MkPagingButtons.vue'; -import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue'; -import MkFolder from '@/components/MkFolder.vue'; -import MkSelect from '@/components/MkSelect.vue'; -import { deviceKind } from '@/scripts/device-kind.js'; import { GridSetting } from '@/components/grid/grid.js'; import { selectFile } from '@/scripts/select-file.js'; import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js'; -import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue'; -import { SortOrder } from '@/components/MkSortOrderEditor.define.js'; import { useLoading } from "@/components/hook/useLoading.js"; type GridItem = { @@ -230,6 +117,11 @@ function setupGrid(): GridSetting { const regex = validators.regex(/^[a-zA-Z0-9_]+$/); const unique = validators.unique(); return { + root: { + noOverflowStyle: true, + rounded: false, + outerBorder: false, + }, row: { showNumber: true, selectable: true, @@ -381,16 +273,22 @@ const customEmojis = ref<Misskey.entities.EmojiDetailedAdmin[]>([]); const allPages = ref<number>(0); const currentPage = ref<number>(0); -const queryName = ref<string | null>(null); -const queryCategory = ref<string | null>(null); -const queryAliases = ref<string | null>(null); -const queryType = ref<string | null>(null); -const queryLicense = ref<string | null>(null); -const queryUpdatedAtFrom = ref<string | null>(null); -const queryUpdatedAtTo = ref<string | null>(null); -const querySensitive = ref<string | null>(null); -const queryLocalOnly = ref<string | null>(null); -const queryRoles = ref<{ id: string, name: string }[]>([]); +const searchQuery = ref<EmojiSearchQuery>({ + name: null, + category: null, + aliases: null, + type: null, + license: null, + updatedAtFrom: null, + updatedAtTo: null, + sensitive: null, + localOnly: null, + roles: [], + sortOrders: [], + limit: 25, +}); +let searchWindowOpening = false; + const previousQuery = ref<string | undefined>(undefined); const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]); const requestLogs = ref<RequestLogItem[]>([]); @@ -399,8 +297,6 @@ const gridItems = ref<GridItem[]>([]); const originGridItems = ref<GridItem[]>([]); const updateButtonDisabled = ref<boolean>(false); -const spMode = computed(() => ['smartphone', 'tablet'].includes(deviceKind)); -const queryRolesText = computed(() => queryRoles.value.map(it => it.name).join(',')); const updatedItemsCount = computed(() => { return gridItems.value.filter((it, idx) => !it.checked && JSON.stringify(it) !== JSON.stringify(originGridItems.value[idx])).length; }); @@ -422,12 +318,11 @@ async function onUpdateButtonClicked() { return; } - const confirm = await os.confirm({ + const { canceled } = await os.confirm({ type: 'info', - title: i18n.ts._customEmojisManager._local._list.confirmUpdateEmojisTitle, text: i18n.tsx._customEmojisManager._local._list.confirmUpdateEmojisDescription({ count: updatedItems.length }), }); - if (confirm.canceled) { + if (canceled) { return; } @@ -458,7 +353,7 @@ async function onUpdateButtonClicked() { if (failedItems.length > 0) { await os.alert({ type: 'error', - title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle, + title: i18n.ts.somethingHappened, text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, }); } @@ -489,12 +384,11 @@ async function onDeleteButtonClicked() { return; } - const confirm = await os.confirm({ + const { canceled } = await os.confirm({ type: 'info', - title: i18n.ts._customEmojisManager._local._list.confirmDeleteEmojisTitle, text: i18n.tsx._customEmojisManager._local._list.confirmDeleteEmojisDescription({ count: deleteItems.length }), }); - if (confirm.canceled) { + if (canceled) { return; } @@ -508,47 +402,35 @@ async function onDeleteButtonClicked() { ); } -function onGridResetButtonClicked() { - refreshGridItems(); -} - -async function onQueryRolesEditClicked() { - const result = await os.selectRole({ - initialRoleIds: queryRoles.value.map(it => it.id), - title: i18n.ts._customEmojisManager._local._list.dialogSelectRoleTitle, - publicOnly: true, +async function onGridResetButtonClicked() { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts.resetAreYouSure, + text: i18n.ts._customEmojisManager._local._list.confirmResetDescription, }); - if (result.canceled) { - return; - } - queryRoles.value = result.result; -} + if (canceled) return; -function onSortOrderUpdate(_sortOrders: SortOrder<GridSortOrderKey>[]) { - sortOrders.value = _sortOrders; + refreshGridItems(); } async function onSearchRequest() { await refreshCustomEmojis(); } -function onQueryResetButtonClicked() { - queryName.value = null; - queryCategory.value = null; - queryAliases.value = null; - queryType.value = null; - queryLicense.value = null; - queryUpdatedAtFrom.value = null; - queryUpdatedAtTo.value = null; - querySensitive.value = null; - queryLocalOnly.value = null; - queryRoles.value = []; -} - async function onPageChanged(pageNumber: number) { + if (updatedItemsCount.value > 0) { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._customEmojisManager._local._list.confirmMovePage, + text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption, + }); + if (canceled) return; + } + currentPage.value = pageNumber; - await refreshCustomEmojis(); + await nextTick(); + refreshCustomEmojis(); } function onGridEvent(event: GridEvent) { @@ -574,19 +456,19 @@ function onGridCellValueChange(event: GridCellValueChangeEvent) { } async function refreshCustomEmojis() { - const limit = 100; + const limit = searchQuery.value.limit; const query: Misskey.entities.V2AdminEmojiListRequest['query'] = { - name: emptyStrToUndefined(queryName.value), - type: emptyStrToUndefined(queryType.value), - aliases: emptyStrToUndefined(queryAliases.value), - category: emptyStrToUndefined(queryCategory.value), - license: emptyStrToUndefined(queryLicense.value), - isSensitive: querySensitive.value ? Boolean(querySensitive.value).valueOf() : undefined, - localOnly: queryLocalOnly.value ? Boolean(queryLocalOnly.value).valueOf() : undefined, - updatedAtFrom: emptyStrToUndefined(queryUpdatedAtFrom.value), - updatedAtTo: emptyStrToUndefined(queryUpdatedAtTo.value), - roleIds: queryRoles.value.map(it => it.id), + name: emptyStrToUndefined(searchQuery.value.name), + type: emptyStrToUndefined(searchQuery.value.type), + aliases: emptyStrToUndefined(searchQuery.value.aliases), + category: emptyStrToUndefined(searchQuery.value.category), + license: emptyStrToUndefined(searchQuery.value.license), + isSensitive: searchQuery.value.sensitive ? Boolean(searchQuery.value.sensitive).valueOf() : undefined, + localOnly: searchQuery.value.localOnly ? Boolean(searchQuery.value.localOnly).valueOf() : undefined, + updatedAtFrom: emptyStrToUndefined(searchQuery.value.updatedAtFrom), + updatedAtTo: emptyStrToUndefined(searchQuery.value.updatedAtTo), + roleIds: searchQuery.value.roles.map(it => it.id), hostType: 'local', }; @@ -635,6 +517,83 @@ onMounted(async () => { await refreshCustomEmojis(); }); +const headerPageMetadata = computed(() => ({ + title: i18n.ts._customEmojisManager._local.tabTitleList, + icon: 'ti ti-icons', +})); + +const headerActions = computed(() => [{ + icon: 'ti ti-search', + text: i18n.ts.search, + handler: () => { + if (searchWindowOpening) return; + searchWindowOpening = true; + const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.search.vue')), { + query: searchQuery.value, + }, { + queryUpdated: (query: EmojiSearchQuery) => { + searchQuery.value = query; + }, + sortOrderUpdated: (orders: SortOrder<GridSortOrderKey>[]) => { + sortOrders.value = orders; + }, + search: () => { + onSearchRequest(); + }, + closed: () => { + dispose(); + searchWindowOpening = false; + }, + }); + }, +}, { + icon: 'ti ti-list-numbers', + text: i18n.ts._customEmojisManager._gridCommon.searchLimit, + handler: (ev: MouseEvent) => { + async function changeSearchLimit(to: number) { + if (updatedItemsCount.value > 0) { + const { canceled } = await os.confirm({ + type: 'warning', + title: i18n.ts._customEmojisManager._local._list.confirmChangeView, + text: i18n.ts._customEmojisManager._local._list.confirmMovePageDesciption, + }); + if (canceled) return; + } + + searchQuery.value.limit = to; + refreshCustomEmojis(); + } + + os.popupMenu([{ + type: 'radioOption', + text: '25', + active: computed(() => searchQuery.value.limit === 25), + action: () => changeSearchLimit(25), + }, { + type: 'radioOption', + text: '50', + active: computed(() => searchQuery.value.limit === 50), + action: () => changeSearchLimit(50), + }, { + type: 'radioOption', + text: '100', + active: computed(() => searchQuery.value.limit === 100), + action: () => changeSearchLimit(100), + }], ev.currentTarget ?? ev.target); + }, +}, { + icon: 'ti ti-notes', + text: i18n.ts._customEmojisManager._gridCommon.registrationLogs, + handler: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('./custom-emojis-manager.local.list.logs.vue')), { + logs: requestLogs.value, + }, { + closed: () => { + dispose(); + }, + }); + } +}]); </script> <style module lang="scss"> @@ -650,77 +609,21 @@ onMounted(async () => { background-color: var(--MI_THEME-infoBg); } -.row1 { - grid-row: 1 / 2; -} - -.row2 { - grid-row: 2 / 3; -} - -.row3 { - grid-row: 3 / 4; -} - -.row4 { - grid-row: 4 / 5; -} - -.col1 { - grid-column: 1 / 2; -} - -.col2 { - grid-column: 2 / 3; -} - -.col3 { - grid-column: 3 / 4; -} - -.searchArea { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: 16px; -} - -.searchAreaSp { - display: flex; - flex-direction: column; - gap: 8px; -} - -.searchButtons { - display: flex; - justify-content: flex-end; - align-items: flex-end; - gap: 8px; -} - -.searchButtonsSp { - display: flex; - justify-content: center; - align-items: center; - gap: 8px; +.main { + height: calc(100vh - var(--MI-stickyTop) - var(--MI-stickyBottom)); + overflow: scroll; } -.gridArea { - padding-top: 8px; - padding-bottom: 8px; +.grid { + width: max-content; + border-bottom: 1px solid var(--MI_THEME-divider); } .footer { background-color: var(--MI_THEME-bg); - position: sticky; - left:0; - bottom:0; - z-index: 1; - // stickyã§è¿½å¾“ã•ã›ã‚‹éƒ½åˆä¸Šã€ãƒ•ãƒƒã‚¿ãƒ¼è‡ªèº«ã§paddingã‚’æŒã¤å¿…è¦ãŒã‚ã‚‹ãŸã‚ã€è¦ªè¦ç´ ã§ç”»ä¸€çš„ã«æŒ‡å®šã—ã¦ã„る分をãƒã‚¬ãƒ†ã‚£ãƒ–マージンã§ç›¸æ®ºã—ã¦ã„ã‚‹ - margin-top: calc(var(--MI-margin) * -1); - margin-bottom: calc(var(--MI-margin) * -1); - padding-top: var(--MI-margin); - padding-bottom: var(--MI-margin); + padding: var(--MI-margin); + border-top: 1px solid var(--MI_THEME-divider); display: grid; grid-template-columns: 1fr 1fr 1fr; diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue index a3de5de569111f268e55848b22eb4ceb73ee6c7e..cc8b625cd5e7357bbf4bc13cd05bbc8b47b3ea45 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue @@ -30,7 +30,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> - <XRegisterLogsFolder :logs="requestLogs"/> + <MkFolder> + <template #icon><i class="ti ti-notes"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template> + <template #caption> + {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }} + </template> + <XRegisterLogs :logs="requestLogs"/> + </MkFolder> <div :class="[$style.uploadBox, [isDragOver ? $style.dragOver : {}]]" @@ -91,7 +98,7 @@ import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js' import { uploadFile } from '@/scripts/upload.js'; import { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import { DroppedFile, extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js'; -import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue'; +import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import { GridSetting } from '@/components/grid/grid.js'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; import { GridRow } from '@/components/grid/row.js'; @@ -245,7 +252,6 @@ const isDragOver = ref<boolean>(false); async function onRegistryClicked() { const dialogSelection = await os.confirm({ type: 'info', - title: i18n.ts._customEmojisManager._local._register.confirmRegisterEmojisTitle, text: i18n.tsx._customEmojisManager._local._register.confirmRegisterEmojisDescription({ count: MAXIMUM_EMOJI_REGISTER_COUNT }), }); @@ -279,7 +285,7 @@ async function onRegistryClicked() { if (failedItems.length > 0) { await os.alert({ type: 'error', - title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle, + title: i18n.ts.somethingHappened, text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, }); } @@ -299,7 +305,6 @@ async function onRegistryClicked() { async function onClearClicked() { const result = await os.confirm({ type: 'warning', - title: i18n.ts._customEmojisManager._local._register.confirmClearEmojisTitle, text: i18n.ts._customEmojisManager._local._register.confirmClearEmojisDescription, }); @@ -314,7 +319,6 @@ async function onDrop(ev: DragEvent) { const droppedFiles = await extractDroppedItems(ev).then(it => flattenDroppedFiles(it)); const confirm = await os.confirm({ type: 'info', - title: i18n.ts._customEmojisManager._local._register.confirmUploadEmojisTitle, text: i18n.tsx._customEmojisManager._local._register.confirmUploadEmojisDescription({ count: droppedFiles.length }), }); if (confirm.canceled) { diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue index ea4303f342fba77a33f094ea3295943de18b5374..6e7e7e53e36af52997dc18a5bd7785a5957c68b9 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.vue @@ -4,33 +4,32 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div class="_gaps" :class="$style.root"> - <MkTab v-model="modeTab" style="margin-bottom: var(--margin);"> - <option value="list">{{ i18n.ts._customEmojisManager._local.tabTitleList }}</option> - <option value="register">{{ i18n.ts._customEmojisManager._local.tabTitleRegister }}</option> - </MkTab> - - <div> - <XListComponent v-if="modeTab === 'list'"/> - <XRegisterComponent v-else/> - </div> -</div> +<MkStickyContainer> + <template #header> + <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs" hideTitle thin/> + </template> + <XListComponent v-if="headerTab === 'list'" key="localList"/> + <MkSpacer v-else key="localRegister"> + <XRegisterComponent/> + </MkSpacer> +</MkStickyContainer> </template> <script setup lang="ts"> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; -import MkTab from '@/components/MkTab.vue'; import XListComponent from '@/pages/admin/custom-emojis-manager.local.list.vue'; import XRegisterComponent from '@/pages/admin/custom-emojis-manager.local.register.vue'; type PageMode = 'list' | 'register'; -const modeTab = ref<PageMode>('list'); -</script> +const headerTab = ref<PageMode>('list'); -<style module lang="scss"> -.root { - padding: var(--MI-margin); -} -</style> +const headerTabs = computed(() => [{ + key: 'list', + title: i18n.ts._customEmojisManager._local.tabTitleList, +}, { + key: 'register', + title: i18n.ts._customEmojisManager._local.tabTitleRegister, +}]); +</script> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue similarity index 62% rename from packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue rename to packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue index f75f6c0da5a8866833e32be9f174a1fd92a346e8..eef55a9f7ed4c7c7dd7a6cc8e08080e1c37b3060 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.logs-folder.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.logs.vue @@ -4,47 +4,38 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkFolder> - <template #icon><i class="ti ti-notes"></i></template> - <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template> - <template #caption> - {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }} - </template> - - <div> - <div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;"> - <MkSwitch v-model="showingSuccessLogs"> - <template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template> - </MkSwitch> - <div> - <div v-if="filteredLogs.length > 0"> - <MkGrid - :data="filteredLogs" - :settings="setupGrid()" - /> - </div> - <div v-else> - {{ i18n.ts._customEmojisManager._logs.failureLogNothing }} - </div> +<div> + <div v-if="logs.length > 0" style="display:flex; flex-direction: column; overflow-y: scroll; gap: 16px;"> + <MkSwitch v-model="showingSuccessLogs"> + <template #label>{{ i18n.ts._customEmojisManager._logs.showSuccessLogSwitch }}</template> + </MkSwitch> + <div> + <div v-if="filteredLogs.length > 0"> + <MkGrid + :data="filteredLogs" + :settings="setupGrid()" + /> + </div> + <div v-else> + {{ i18n.ts._customEmojisManager._logs.failureLogNothing }} </div> - </div> - <div v-else> - {{ i18n.ts._customEmojisManager._logs.logNothing }} </div> </div> -</MkFolder> + <div v-else> + {{ i18n.ts._customEmojisManager._logs.logNothing }} + </div> +</div> </template> <script setup lang="ts"> - import { computed, ref, toRefs } from 'vue'; import { i18n } from '@/i18n.js'; -import { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; import MkGrid from '@/components/grid/MkGrid.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import { GridSetting } from '@/components/grid/grid.js'; import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js'; -import MkFolder from '@/components/MkFolder.vue'; + +import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js'; +import type { GridSetting } from '@/components/grid/grid.js'; function setupGrid(): GridSetting { return { @@ -94,9 +85,4 @@ const filteredLogs = computed(() => { const forceShowing = showingSuccessLogs.value; return logs.value.filter((log) => forceShowing || log.failed); }); - </script> - -<style module lang="scss"> - -</style> diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue index 14a3b71e5330f400d2a813b29a29ed3d1e4c5584..eecf8d739099dbc863816cd00e25fc173d0bdcdb 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue @@ -64,6 +64,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </div> + <hr> + <MkFolder :spacerMax="8" :spacerMin="8"> <template #icon><i class="ti ti-arrows-sort"></i></template> <template #label>{{ i18n.ts._customEmojisManager._gridCommon.sortOrder }}</template> @@ -74,6 +76,14 @@ SPDX-License-Identifier: AGPL-3.0-only /> </MkFolder> + <MkInput + v-model="queryLimit" + type="number" + :max="100" + > + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.searchLimit }}</template> + </MkInput> + <div :class="[[spMode ? $style.searchButtonsSp : $style.searchButtons]]"> <MkButton primary @click="onSearchRequest"> {{ i18n.ts.search }} @@ -85,7 +95,14 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </MkFolder> - <XRegisterLogsFolder :logs="requestLogs"/> + <MkFolder> + <template #icon><i class="ti ti-notes"></i></template> + <template #label>{{ i18n.ts._customEmojisManager._gridCommon.registrationLogs }}</template> + <template #caption> + {{ i18n.ts._customEmojisManager._gridCommon.registrationLogsCaption }} + </template> + <XRegisterLogs :logs="requestLogs"/> + </MkFolder> <component :is="loadingHandler.component.value" v-if="loadingHandler.showing.value"/> <template v-else> @@ -139,7 +156,7 @@ import { } from '@/pages/admin/custom-emojis-manager.impl.js'; import { GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js'; import MkFolder from '@/components/MkFolder.vue'; -import XRegisterLogsFolder from '@/pages/admin/custom-emojis-manager.logs-folder.vue'; +import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue'; import * as os from '@/os.js'; import { GridSetting } from '@/components/grid/grid.js'; import { deviceKind } from '@/scripts/device-kind.js'; @@ -246,6 +263,7 @@ const queryHost = ref<string | null>(null); const queryLicense = ref<string | null>(null); const queryUri = ref<string | null>(null); const queryPublicUrl = ref<string | null>(null); +const queryLimit = ref<number>(25); const previousQuery = ref<string | undefined>(undefined); const sortOrders = ref<SortOrder<GridSortOrderKey>[]>([]); const requestLogs = ref<RequestLogItem[]>([]); @@ -325,7 +343,7 @@ async function importEmojis(targets: GridItem[]) { if (failedItems.length > 0) { await os.alert({ type: 'error', - title: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedTitle, + title: i18n.ts.somethingHappened, text: i18n.ts._customEmojisManager._gridCommon.alertEmojisRegisterFailedDescription, }); } @@ -355,7 +373,7 @@ async function refreshCustomEmojis() { } const result = await loadingHandler.scope(() => misskeyApi('v2/admin/emoji/list', { - limit: 100, + limit: queryLimit.value, query: query, page: currentPage.value, sortKeys: sortOrders.value.map(({ key, direction }) => `${direction}${key}`) as never[], diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue index a952a5a3d1cd94b852dfed189e9e015f1e0aeb9b..fb930064ffaec20785397fdb0217d7ef5052a348 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue @@ -5,12 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div> - <!-- コンテナãŒå…¥ã‚Œåã«ãªã‚‹ã®ã§z-indexãŒè¢«ã‚‰ãªã„よã†å¤§ãã‚ã®æ•°å€¤ã‚’è¨å®šã™ã‚‹--> - <MkStickyContainer :headerZIndex="2000"> + <MkStickyContainer> <template #header> <MkPageHeader v-model:tab="headerTab" :tabs="headerTabs"/> </template> - <XGridLocalComponent v-if="headerTab === 'local'"/> + <XGridLocalComponent v-if="headerTab === 'local'" :class="$style.local"/> <XGridRemoteComponent v-else/> </MkStickyContainer> </div> @@ -40,5 +39,13 @@ const headerTabs = computed(() => [{ definePageMetadata(computed(() => ({ title: i18n.ts.customEmojis, icon: 'ti ti-icons', + needWideArea: true, }))); </script> + +<style lang="css" module> +.local { + height: calc(100dvh - var(--MI-stickyTop) - var(--MI-stickyBottom)); + overflow: clip; +} +</style> diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 969ca8b9e84ab5b8edb27d64e65d44a48f9fb41e..ea5fa457f2303d1f908cfed20a7b9dcceeba189d 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -34,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { onActivated, onMounted, onUnmounted, provide, watch, ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import MkSuperMenu from '@/components/MkSuperMenu.vue'; +import type { SuperMenuDef } from '@/components/MkSuperMenu.vue'; import MkInfo from '@/components/MkInfo.vue'; import { instance } from '@/instance.js'; import { lookup } from '@/scripts/lookup.js'; @@ -55,7 +56,7 @@ const indexInfo = { provide('shouldOmitHeaderTitle', false); -const INFO = ref(indexInfo); +const INFO = ref<PageMetadata>(indexInfo); const childInfo = ref<null | PageMetadata>(null); const narrow = ref(false); const view = ref(null); @@ -81,7 +82,7 @@ const ro = new ResizeObserver((entries, observer) => { narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD; }); -const menuDef = computed(() => [{ +const menuDef = computed<SuperMenuDef[]>(() => [{ title: i18n.ts.quickAction, items: [{ type: 'button', @@ -89,7 +90,7 @@ const menuDef = computed(() => [{ text: i18n.ts.lookup, action: adminLookup, }, ...(instance.disableRegistration ? [{ - type: 'button', + type: 'button' as const, icon: 'ti ti-user-plus', text: i18n.ts.createInviteCode, action: invite, @@ -333,12 +334,14 @@ defineExpose({ height: 100%; > .nav { + position: sticky; + top: 0; width: 32%; max-width: 280px; box-sizing: border-box; border-right: solid 0.5px var(--MI_THEME-divider); overflow: auto; - height: 100%; + height: 100dvh; } > .main { diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue index 96a95f1635867eaf949c6034f9b145f2a0f8c0bd..b5a6d719d1f8fe5ebda145a7418aff50bb7ed412 100644 --- a/packages/frontend/src/pages/settings/index.vue +++ b/packages/frontend/src/pages/settings/index.vue @@ -44,7 +44,7 @@ const indexInfo = { icon: 'ti ti-settings', hideHeader: true, }; -const INFO = ref(indexInfo); +const INFO = ref<PageMetadata>(indexInfo); const el = shallowRef<HTMLElement | null>(null); const childInfo = ref<null | PageMetadata>(null); diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index d739c2e1cddc656cd281e0541f3ffeabfbe9c892..94998b7be6e352c24cf2e78ff3c922c701c97ec9 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only <XWidgets/> </div> - <button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> + <button v-if="!isDesktop && !pageMetadata?.needWideArea && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button> <div v-if="isMobile" ref="navFooter" :class="$style.nav"> <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>