diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-06-20 17:38:49 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-06-20 17:38:49 +0900 |
| commit | 699f24f3dcdb156838eb70602885c0b2cdd02cbc (patch) | |
| tree | 45b28eeadbb7d9e7f3847bd04f75ed010153619a /packages/client/src/pages/admin | |
| parent | refactor: チャットルームをComposition API化 (#8850) (diff) | |
| download | misskey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.tar.gz misskey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.tar.bz2 misskey-699f24f3dcdb156838eb70602885c0b2cdd02cbc.zip | |
refactor(client): Refine routing (#8846)
Diffstat (limited to 'packages/client/src/pages/admin')
21 files changed, 1136 insertions, 790 deletions
diff --git a/packages/client/src/pages/admin/_header_.vue b/packages/client/src/pages/admin/_header_.vue new file mode 100644 index 0000000000..9e11d065d9 --- /dev/null +++ b/packages/client/src/pages/admin/_header_.vue @@ -0,0 +1,249 @@ +<template> +<div ref="el" class="fdidabkc" :style="{ background: bg }" @click="onClick"> + <template v-if="metadata"> + <div class="titleContainer" @click="showTabsPopup"> + <i v-if="metadata.icon" class="icon" :class="metadata.icon"></i> + + <div class="title"> + <div class="title">{{ metadata.title }}</div> + </div> + </div> + <div class="tabs"> + <button v-for="tab in tabs" v-tooltip="tab.title" class="tab _button" :class="{ active: tab.active }" @click="tab.onClick"> + <i v-if="tab.icon" class="icon" :class="tab.icon"></i> + <span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span> + </button> + </div> + </template> + <div class="buttons right"> + <template v-if="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> + <button v-else v-tooltip="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button> + </template> + </template> + </div> +</div> +</template> + +<script lang="ts" setup> +import { computed, onMounted, onUnmounted, ref, inject } from 'vue'; +import tinycolor from 'tinycolor2'; +import { popupMenu } from '@/os'; +import { url } from '@/config'; +import { scrollToTop } from '@/scripts/scroll'; +import MkButton from '@/components/ui/button.vue'; +import { i18n } from '@/i18n'; +import { globalEvents } from '@/events'; +import { injectPageMetadata, PageMetadata } from '@/scripts/page-metadata'; + +const props = defineProps<{ + tabs?: { + title: string; + active: boolean; + icon?: string; + iconOnly?: boolean; + onClick: () => void; + }[]; + actions?: { + text: string; + icon: string; + asFullButton?: boolean; + handler: (ev: MouseEvent) => void; + }[]; + thin?: boolean; +}>(); + +const metadata = injectPageMetadata(); + +const el = ref<HTMLElement>(null); +const bg = ref(null); +const height = ref(0); +const hasTabs = computed(() => { + return props.tabs && props.tabs.length > 0; +}); + +const showTabsPopup = (ev: MouseEvent) => { + if (!hasTabs.value) return; + if (!narrow.value) return; + ev.preventDefault(); + ev.stopPropagation(); + const menu = props.tabs.map(tab => ({ + text: tab.title, + icon: tab.icon, + action: tab.onClick, + })); + popupMenu(menu, ev.currentTarget ?? ev.target); +}; + +const preventDrag = (ev: TouchEvent) => { + ev.stopPropagation(); +}; + +const onClick = () => { + scrollToTop(el.value, { behavior: 'smooth' }); +}; + +const calcBg = () => { + const rawBg = metadata?.bg || 'var(--bg)'; + const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg); + tinyBg.setAlpha(0.85); + bg.value = tinyBg.toRgbString(); +}; + +onMounted(() => { + calcBg(); + globalEvents.on('themeChanged', calcBg); +}); + +onUnmounted(() => { + globalEvents.off('themeChanged', calcBg); +}); +</script> + +<style lang="scss" scoped> +.fdidabkc { + --height: 60px; + display: flex; + position: sticky; + top: var(--stickyTop, 0); + z-index: 1000; + width: 100%; + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + + > .buttons { + --margin: 8px; + display: flex; + align-items: center; + height: var(--height); + margin: 0 var(--margin); + + &.right { + margin-left: auto; + } + + &:empty { + width: var(--height); + } + + > .button { + display: flex; + align-items: center; + justify-content: center; + height: calc(var(--height) - (var(--margin) * 2)); + width: calc(var(--height) - (var(--margin) * 2)); + box-sizing: border-box; + position: relative; + border-radius: 5px; + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &.highlighted { + color: var(--accent); + } + } + + > .fullButton { + & + .fullButton { + margin-left: 12px; + } + } + } + + > .titleContainer { + display: flex; + align-items: center; + max-width: 400px; + overflow: auto; + white-space: nowrap; + text-align: left; + font-weight: bold; + flex-shrink: 0; + margin-left: 24px; + + > .avatar { + $size: 32px; + display: inline-block; + width: $size; + height: $size; + vertical-align: bottom; + margin: 0 8px; + pointer-events: none; + } + + > .icon { + margin-right: 8px; + } + + > .title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.1; + + > .subtitle { + opacity: 0.6; + font-size: 0.8em; + font-weight: normal; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + &.activeTab { + text-align: center; + + > .chevron { + display: inline-block; + margin-left: 6px; + } + } + } + } + } + + > .tabs { + margin-left: 16px; + font-size: 0.8em; + overflow: auto; + white-space: nowrap; + + > .tab { + display: inline-block; + position: relative; + padding: 0 10px; + height: 100%; + font-weight: normal; + opacity: 0.7; + + &:hover { + opacity: 1; + } + + &.active { + opacity: 1; + + &:after { + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + right: 0; + margin: 0 auto; + width: 100%; + height: 3px; + background: var(--accent); + } + } + + > .icon + .title { + margin-left: 8px; + } + } + } +} +</style> diff --git a/packages/client/src/pages/admin/abuses.vue b/packages/client/src/pages/admin/abuses.vue index e1d0361c0b..2b6dadf7c6 100644 --- a/packages/client/src/pages/admin/abuses.vue +++ b/packages/client/src/pages/admin/abuses.vue @@ -1,28 +1,31 @@ <template> -<div class="lcixvhis"> - <div class="_section reports"> - <div class="_content"> - <div class="inputs" style="display: flex;"> - <MkSelect v-model="state" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.state }}</template> - <option value="all">{{ $ts.all }}</option> - <option value="unresolved">{{ $ts.unresolved }}</option> - <option value="resolved">{{ $ts.resolved }}</option> - </MkSelect> - <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.reporteeOrigin }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.reporterOrigin }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - </div> - <!-- TODO +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="900"> + <div class="lcixvhis"> + <div class="_section reports"> + <div class="_content"> + <div class="inputs" style="display: flex;"> + <MkSelect v-model="state" style="margin: 0; flex: 1;"> + <template #label>{{ $ts.state }}</template> + <option value="all">{{ $ts.all }}</option> + <option value="unresolved">{{ $ts.unresolved }}</option> + <option value="resolved">{{ $ts.resolved }}</option> + </MkSelect> + <MkSelect v-model="targetUserOrigin" style="margin: 0; flex: 1;"> + <template #label>{{ $ts.reporteeOrigin }}</template> + <option value="combined">{{ $ts.all }}</option> + <option value="local">{{ $ts.local }}</option> + <option value="remote">{{ $ts.remote }}</option> + </MkSelect> + <MkSelect v-model="reporterOrigin" style="margin: 0; flex: 1;"> + <template #label>{{ $ts.reporterOrigin }}</template> + <option value="combined">{{ $ts.all }}</option> + <option value="local">{{ $ts.local }}</option> + <option value="remote">{{ $ts.remote }}</option> + </MkSelect> + </div> + <!-- TODO <div class="inputs" style="display: flex; padding-top: 1.2em;"> <MkInput v-model="searchUsername" style="margin: 0; flex: 1;" type="text" spellcheck="false"> <span>{{ $ts.username }}</span> @@ -33,24 +36,27 @@ </div> --> - <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> - <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> - </MkPagination> + <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" style="margin-top: var(--margin);"> + <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/> + </MkPagination> + </div> + </div> </div> - </div> -</div> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { computed } from 'vue'; +import XHeader from './_header_.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; import MkPagination from '@/components/ui/pagination.vue'; import XAbuseReport from '@/components/abuse-report.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let reports = $ref<InstanceType<typeof MkPagination>>(); @@ -74,12 +80,14 @@ function resolved(reportId) { reports.removeItem(item => item.id === reportId); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.abuseReports, - icon: 'fas fa-exclamation-circle', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.abuseReports, + icon: 'fas fa-exclamation-circle', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/ads.vue b/packages/client/src/pages/admin/ads.vue index b18e08db96..05557469e7 100644 --- a/packages/client/src/pages/admin/ads.vue +++ b/packages/client/src/pages/admin/ads.vue @@ -1,21 +1,23 @@ <template> -<MkSpacer :content-max="900"> - <div class="uqshojas"> - <div v-for="ad in ads" class="_panel _formRoot ad"> - <MkAd v-if="ad.url" :specify="ad"/> - <MkInput v-model="ad.url" type="url" class="_formBlock"> - <template #label>URL</template> - </MkInput> - <MkInput v-model="ad.imageUrl" class="_formBlock"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <FormRadios v-model="ad.place" class="_formBlock"> - <template #label>Form</template> - <option value="square">square</option> - <option value="horizontal">horizontal</option> - <option value="horizontal-big">horizontal-big</option> - </FormRadios> - <!-- +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="900"> + <div class="uqshojas"> + <div v-for="ad in ads" class="_panel _formRoot ad"> + <MkAd v-if="ad.url" :specify="ad"/> + <MkInput v-model="ad.url" type="url" class="_formBlock"> + <template #label>URL</template> + </MkInput> + <MkInput v-model="ad.imageUrl" class="_formBlock"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <FormRadios v-model="ad.place" class="_formBlock"> + <template #label>Form</template> + <option value="square">square</option> + <option value="horizontal">horizontal</option> + <option value="horizontal-big">horizontal-big</option> + </FormRadios> + <!-- <div style="margin: 32px 0;"> {{ i18n.ts.priority }} <MkRadio v-model="ad.priority" value="high">{{ i18n.ts.high }}</MkRadio> @@ -23,36 +25,38 @@ <MkRadio v-model="ad.priority" value="low">{{ i18n.ts.low }}</MkRadio> </div> --> - <FormSplit> - <MkInput v-model="ad.ratio" type="number"> - <template #label>{{ i18n.ts.ratio }}</template> - </MkInput> - <MkInput v-model="ad.expiresAt" type="date"> - <template #label>{{ i18n.ts.expiration }}</template> - </MkInput> - </FormSplit> - <MkTextarea v-model="ad.memo" class="_formBlock"> - <template #label>{{ i18n.ts.memo }}</template> - </MkTextarea> - <div class="buttons _formBlock"> - <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> - <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> + <FormSplit> + <MkInput v-model="ad.ratio" type="number"> + <template #label>{{ i18n.ts.ratio }}</template> + </MkInput> + <MkInput v-model="ad.expiresAt" type="date"> + <template #label>{{ i18n.ts.expiration }}</template> + </MkInput> + </FormSplit> + <MkTextarea v-model="ad.memo" class="_formBlock"> + <template #label>{{ i18n.ts.memo }}</template> + </MkTextarea> + <div class="buttons _formBlock"> + <MkButton class="button" inline primary style="margin-right: 12px;" @click="save(ad)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> + <MkButton class="button" inline danger @click="remove(ad)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> + </div> </div> </div> - </div> -</MkSpacer> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; import FormRadios from '@/components/form/radios.vue'; import FormSplit from '@/components/form/split.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let ads: any[] = $ref([]); @@ -81,7 +85,7 @@ function remove(ad) { if (canceled) return; ads = ads.filter(x => x !== ad); os.apiWithDialog('admin/ad/delete', { - id: ad.id + id: ad.id, }); }); } @@ -90,28 +94,29 @@ function save(ad) { if (ad.id == null) { os.apiWithDialog('admin/ad/create', { ...ad, - expiresAt: new Date(ad.expiresAt).getTime() + expiresAt: new Date(ad.expiresAt).getTime(), }); } else { os.apiWithDialog('admin/ad/update', { ...ad, - expiresAt: new Date(ad.expiresAt).getTime() + expiresAt: new Date(ad.expiresAt).getTime(), }); } } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.ads, - icon: 'fas fa-audio-description', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: i18n.ts.add, - handler: add, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.add, + handler: add, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.ads, + icon: 'fas fa-audio-description', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/announcements.vue b/packages/client/src/pages/admin/announcements.vue index 97774975de..025897d093 100644 --- a/packages/client/src/pages/admin/announcements.vue +++ b/packages/client/src/pages/admin/announcements.vue @@ -1,34 +1,40 @@ <template> -<div class="ztgjmzrw"> - <section v-for="announcement in announcements" class="_card _gap announcements"> - <div class="_content announcement"> - <MkInput v-model="announcement.title"> - <template #label>{{ i18n.ts.title }}</template> - </MkInput> - <MkTextarea v-model="announcement.text"> - <template #label>{{ i18n.ts.text }}</template> - </MkTextarea> - <MkInput v-model="announcement.imageUrl"> - <template #label>{{ i18n.ts.imageUrl }}</template> - </MkInput> - <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> - <div class="buttons"> - <MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> - <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> - </div> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="900"> + <div class="ztgjmzrw"> + <section v-for="announcement in announcements" class="_card _gap announcements"> + <div class="_content announcement"> + <MkInput v-model="announcement.title"> + <template #label>{{ i18n.ts.title }}</template> + </MkInput> + <MkTextarea v-model="announcement.text"> + <template #label>{{ i18n.ts.text }}</template> + </MkTextarea> + <MkInput v-model="announcement.imageUrl"> + <template #label>{{ i18n.ts.imageUrl }}</template> + </MkInput> + <p v-if="announcement.reads">{{ i18n.t('nUsersRead', { n: announcement.reads }) }}</p> + <div class="buttons"> + <MkButton class="button" inline primary @click="save(announcement)"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> + <MkButton class="button" inline @click="remove(announcement)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> + </div> + </div> + </section> </div> - </section> -</div> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkTextarea from '@/components/form/textarea.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let announcements: any[] = $ref([]); @@ -41,7 +47,7 @@ function add() { id: null, title: '', text: '', - imageUrl: null + imageUrl: null, }); } @@ -61,41 +67,42 @@ function save(announcement) { os.api('admin/announcements/create', announcement).then(() => { os.alert({ type: 'success', - text: i18n.ts.saved + text: i18n.ts.saved, }); }).catch(err => { os.alert({ type: 'error', - text: err + text: err, }); }); } else { os.api('admin/announcements/update', announcement).then(() => { os.alert({ type: 'success', - text: i18n.ts.saved + text: i18n.ts.saved, }); }).catch(err => { os.alert({ type: 'error', - text: err + text: err, }); }); } } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.announcements, - icon: 'fas fa-broadcast-tower', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: i18n.ts.add, - handler: add, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.add, + handler: add, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.announcements, + icon: 'fas fa-broadcast-tower', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/bot-protection.vue b/packages/client/src/pages/admin/bot-protection.vue index 30fee5015a..d2e7919b4f 100644 --- a/packages/client/src/pages/admin/bot-protection.vue +++ b/packages/client/src/pages/admin/bot-protection.vue @@ -51,7 +51,6 @@ import FormButton from '@/components/ui/button.vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormSlot from '@/components/form/slot.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; const MkCaptcha = defineAsyncComponent(() => import('@/components/captcha.vue')); diff --git a/packages/client/src/pages/admin/database.vue b/packages/client/src/pages/admin/database.vue index d3519922b1..b9c5f9e393 100644 --- a/packages/client/src/pages/admin/database.vue +++ b/packages/client/src/pages/admin/database.vue @@ -1,12 +1,13 @@ -<template> -<MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> +<template><MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="800" :margin-min="16" :margin-max="32"> <FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory"> <MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;"> <template #key>{{ table[0] }}</template> <template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template> </MkKeyValue> </FormSuspense> -</MkSpacer> +</MkSpacer></MkStickyContainer> </template> <script lang="ts" setup> @@ -14,18 +15,20 @@ import { } from 'vue'; import FormSuspense from '@/components/form/suspense.vue'; import MkKeyValue from '@/components/key-value.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import bytes from '@/filters/bytes'; import number from '@/filters/number'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; const databasePromiseFactory = () => os.api('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size)); -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.database, - icon: 'fas fa-database', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.database, + icon: 'fas fa-database', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/email-settings.vue b/packages/client/src/pages/admin/email-settings.vue index aa13043193..5487c5f333 100644 --- a/packages/client/src/pages/admin/email-settings.vue +++ b/packages/client/src/pages/admin/email-settings.vue @@ -1,49 +1,53 @@ <template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> - <FormSuspense :p="init"> - <div class="_formRoot"> - <FormSwitch v-model="enableEmail" class="_formBlock"> - <template #label>{{ i18n.ts.enableEmail }}</template> - <template #caption>{{ i18n.ts.emailConfigInfo }}</template> - </FormSwitch> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + <div class="_formRoot"> + <FormSwitch v-model="enableEmail" class="_formBlock"> + <template #label>{{ i18n.ts.enableEmail }}</template> + <template #caption>{{ i18n.ts.emailConfigInfo }}</template> + </FormSwitch> - <template v-if="enableEmail"> - <FormInput v-model="email" type="email" class="_formBlock"> - <template #label>{{ i18n.ts.emailAddress }}</template> - </FormInput> + <template v-if="enableEmail"> + <FormInput v-model="email" type="email" class="_formBlock"> + <template #label>{{ i18n.ts.emailAddress }}</template> + </FormInput> - <FormSection> - <template #label>{{ i18n.ts.smtpConfig }}</template> - <FormSplit :min-width="280"> - <FormInput v-model="smtpHost" class="_formBlock"> - <template #label>{{ i18n.ts.smtpHost }}</template> - </FormInput> - <FormInput v-model="smtpPort" type="number" class="_formBlock"> - <template #label>{{ i18n.ts.smtpPort }}</template> - </FormInput> - </FormSplit> - <FormSplit :min-width="280"> - <FormInput v-model="smtpUser" class="_formBlock"> - <template #label>{{ i18n.ts.smtpUser }}</template> - </FormInput> - <FormInput v-model="smtpPass" type="password" class="_formBlock"> - <template #label>{{ i18n.ts.smtpPass }}</template> - </FormInput> - </FormSplit> - <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> - <FormSwitch v-model="smtpSecure" class="_formBlock"> - <template #label>{{ i18n.ts.smtpSecure }}</template> - <template #caption>{{ i18n.ts.smtpSecureInfo }}</template> - </FormSwitch> - </FormSection> - </template> - </div> - </FormSuspense> -</MkSpacer> + <FormSection> + <template #label>{{ i18n.ts.smtpConfig }}</template> + <FormSplit :min-width="280"> + <FormInput v-model="smtpHost" class="_formBlock"> + <template #label>{{ i18n.ts.smtpHost }}</template> + </FormInput> + <FormInput v-model="smtpPort" type="number" class="_formBlock"> + <template #label>{{ i18n.ts.smtpPort }}</template> + </FormInput> + </FormSplit> + <FormSplit :min-width="280"> + <FormInput v-model="smtpUser" class="_formBlock"> + <template #label>{{ i18n.ts.smtpUser }}</template> + </FormInput> + <FormInput v-model="smtpPass" type="password" class="_formBlock"> + <template #label>{{ i18n.ts.smtpPass }}</template> + </FormInput> + </FormSplit> + <FormInfo class="_formBlock">{{ i18n.ts.emptyToDisableSmtpAuth }}</FormInfo> + <FormSwitch v-model="smtpSecure" class="_formBlock"> + <template #label>{{ i18n.ts.smtpSecure }}</template> + <template #caption>{{ i18n.ts.smtpSecureInfo }}</template> + </FormSwitch> + </FormSection> + </template> + </div> + </FormSuspense> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormInfo from '@/components/ui/info.vue'; @@ -51,9 +55,9 @@ import FormSuspense from '@/components/form/suspense.vue'; import FormSplit from '@/components/form/split.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance, instance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let enableEmail: boolean = $ref(false); let email: any = $ref(null); @@ -78,13 +82,13 @@ async function testEmail() { const { canceled, result: destination } = await os.inputText({ title: i18n.ts.destination, type: 'email', - placeholder: instance.maintainerEmail + placeholder: instance.maintainerEmail, }); if (canceled) return; os.apiWithDialog('admin/send-email', { to: destination, subject: 'Test email', - text: 'Yo' + text: 'Yo', }); } @@ -102,21 +106,22 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.emailServer, - icon: 'fas fa-envelope', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - text: i18n.ts.testEmail, - handler: testEmail, - }, { - asFullButton: true, - icon: 'fas fa-check', - text: i18n.ts.save, - handler: save, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + text: i18n.ts.testEmail, + handler: testEmail, +}, { + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.emailServer, + icon: 'fas fa-envelope', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/emojis.vue b/packages/client/src/pages/admin/emojis.vue index 8ca5b3d65c..9d6b56dbc5 100644 --- a/packages/client/src/pages/admin/emojis.vue +++ b/packages/client/src/pages/admin/emojis.vue @@ -1,69 +1,75 @@ <template> -<MkSpacer :content-max="900"> - <div class="ogwlenmc"> - <div v-if="tab === 'local'" class="local"> - <MkInput v-model="query" :debounce="true" type="search"> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.search }}</template> - </MkInput> - <MkSwitch v-model="selectMode" style="margin: 8px 0;"> - <template #label>Select mode</template> - </MkSwitch> - <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <MkButton inline @click="selectAll">Select all</MkButton> - <MkButton inline @click="setCategoryBulk">Set category</MkButton> - <MkButton inline @click="addTagBulk">Add tag</MkButton> - <MkButton inline @click="removeTagBulk">Remove tag</MkButton> - <MkButton inline @click="setTagBulk">Set tag</MkButton> - <MkButton inline danger @click="delBulk">Delete</MkButton> - </div> - <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> - <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> - <template v-slot="{items}"> - <div class="ldhfsamy"> - <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.category }}</div> - </div> - </button> +<div> + <MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="900"> + <div class="ogwlenmc"> + <div v-if="tab === 'local'" class="local"> + <MkInput v-model="query" :debounce="true" type="search"> + <template #prefix><i class="fas fa-search"></i></template> + <template #label>{{ $ts.search }}</template> + </MkInput> + <MkSwitch v-model="selectMode" style="margin: 8px 0;"> + <template #label>Select mode</template> + </MkSwitch> + <div v-if="selectMode" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkButton inline @click="selectAll">Select all</MkButton> + <MkButton inline @click="setCategoryBulk">Set category</MkButton> + <MkButton inline @click="addTagBulk">Add tag</MkButton> + <MkButton inline @click="removeTagBulk">Remove tag</MkButton> + <MkButton inline @click="setTagBulk">Set tag</MkButton> + <MkButton inline danger @click="delBulk">Delete</MkButton> </div> - </template> - </MkPagination> - </div> + <MkPagination ref="emojisPaginationComponent" :pagination="pagination"> + <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> + <template #default="{items}"> + <div class="ldhfsamy"> + <button v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" :class="{ selected: selectedEmojis.includes(emoji.id) }" @click="selectMode ? toggleSelect(emoji) : edit(emoji)"> + <img :src="emoji.url" class="img" :alt="emoji.name"/> + <div class="body"> + <div class="name _monospace">{{ emoji.name }}</div> + <div class="info">{{ emoji.category }}</div> + </div> + </button> + </div> + </template> + </MkPagination> + </div> - <div v-else-if="tab === 'remote'" class="remote"> - <FormSplit> - <MkInput v-model="queryRemote" :debounce="true" type="search"> - <template #prefix><i class="fas fa-search"></i></template> - <template #label>{{ $ts.search }}</template> - </MkInput> - <MkInput v-model="host" :debounce="true"> - <template #label>{{ $ts.host }}</template> - </MkInput> - </FormSplit> - <MkPagination :pagination="remotePagination"> - <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> - <template v-slot="{items}"> - <div class="ldhfsamy"> - <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> - <img :src="emoji.url" class="img" :alt="emoji.name"/> - <div class="body"> - <div class="name _monospace">{{ emoji.name }}</div> - <div class="info">{{ emoji.host }}</div> + <div v-else-if="tab === 'remote'" class="remote"> + <FormSplit> + <MkInput v-model="queryRemote" :debounce="true" type="search"> + <template #prefix><i class="fas fa-search"></i></template> + <template #label>{{ $ts.search }}</template> + </MkInput> + <MkInput v-model="host" :debounce="true"> + <template #label>{{ $ts.host }}</template> + </MkInput> + </FormSplit> + <MkPagination :pagination="remotePagination"> + <template #empty><span>{{ $ts.noCustomEmojis }}</span></template> + <template #default="{items}"> + <div class="ldhfsamy"> + <div v-for="emoji in items" :key="emoji.id" class="emoji _panel _button" @click="remoteMenu(emoji, $event)"> + <img :src="emoji.url" class="img" :alt="emoji.name"/> + <div class="body"> + <div class="name _monospace">{{ emoji.name }}</div> + <div class="info">{{ emoji.host }}</div> + </div> + </div> </div> - </div> - </div> - </template> - </MkPagination> - </div> - </div> -</MkSpacer> + </template> + </MkPagination> + </div> + </div> + </MkSpacer> + </MkStickyContainer> +</div> </template> <script lang="ts" setup> import { computed, defineAsyncComponent, defineComponent, ref, toRef } from 'vue'; +import XHeader from './_header_.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkPagination from '@/components/ui/pagination.vue'; @@ -72,8 +78,8 @@ import MkSwitch from '@/components/form/switch.vue'; import FormSplit from '@/components/form/split.vue'; import { selectFile, selectFiles } from '@/scripts/select-file'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; const emojisPaginationComponent = ref<InstanceType<typeof MkPagination>>(); @@ -131,13 +137,13 @@ const add = async (ev: MouseEvent) => { const edit = (emoji) => { os.popup(defineAsyncComponent(() => import('./emoji-edit-dialog.vue')), { - emoji: emoji + emoji: emoji, }, { done: result => { if (result.updated) { emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ ...oldEmoji, - ...result.updated + ...result.updated, })); } else if (result.deleted) { emojisPaginationComponent.value.removeItem((item) => item.id === emoji.id); @@ -159,7 +165,7 @@ const remoteMenu = (emoji, ev: MouseEvent) => { }, { text: i18n.ts.import, icon: 'fas fa-plus', - action: () => { im(emoji); } + action: () => { im(emoji); }, }], ev.currentTarget ?? ev.target); }; @@ -181,7 +187,7 @@ const menu = (ev: MouseEvent) => { text: err.message, }); }); - } + }, }, { icon: 'fas fa-upload', text: i18n.ts.import, @@ -201,7 +207,7 @@ const menu = (ev: MouseEvent) => { text: err.message, }); }); - } + }, }], ev.currentTarget ?? ev.target); }; @@ -265,31 +271,31 @@ const delBulk = async () => { emojisPaginationComponent.value.reload(); }; -defineExpose({ - [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.ts.customEmojis, - icon: 'fas fa-laugh', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: i18n.ts.addEmoji, - handler: add, - }, { - icon: 'fas fa-ellipsis-h', - handler: menu, - }], - tabs: [{ - active: tab.value === 'local', - title: i18n.ts.local, - onClick: () => { tab.value = 'local'; }, - }, { - active: tab.value === 'remote', - title: i18n.ts.remote, - onClick: () => { tab.value = 'remote'; }, - },] - })), -}); +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.addEmoji, + handler: add, +}, { + icon: 'fas fa-ellipsis-h', + handler: menu, +}]); + +const headerTabs = $computed(() => [{ + active: tab.value === 'local', + title: i18n.ts.local, + onClick: () => { tab.value = 'local'; }, +}, { + active: tab.value === 'remote', + title: i18n.ts.remote, + onClick: () => { tab.value = 'remote'; }, +}]); + +definePageMetadata(computed(() => ({ + title: i18n.ts.customEmojis, + icon: 'fas fa-laugh', + bg: 'var(--bg)', +}))); </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/admin/files.vue b/packages/client/src/pages/admin/files.vue index 9350911b60..18bf4f9a8c 100644 --- a/packages/client/src/pages/admin/files.vue +++ b/packages/client/src/pages/admin/files.vue @@ -1,50 +1,58 @@ <template> -<div class="xrmjdkdw"> - <div> - <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> - <MkSelect v-model="origin" style="margin: 0; flex: 1;"> - <template #label>{{ $ts.instance }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> - <template #label>{{ $ts.host }}</template> - </MkInput> - </div> - <div class="inputs" style="display: flex; padding-top: 1.2em;"> - <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> - <template #label>MIME type</template> - </MkInput> - </div> - <MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> - <button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _panel _button" @click="show(file, $event)"> - <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> - <div v-if="viewMode === 'list'" class="body"> - <div> - <small style="opacity: 0.7;">{{ file.name }}</small> +<div> + <MkStickyContainer> + <template #header><XHeader :actions="headerActions"/></template> + <MkSpacer :content-max="900"> + <div class="xrmjdkdw"> + <div> + <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;"> + <MkSelect v-model="origin" style="margin: 0; flex: 1;"> + <template #label>{{ $ts.instance }}</template> + <option value="combined">{{ $ts.all }}</option> + <option value="local">{{ $ts.local }}</option> + <option value="remote">{{ $ts.remote }}</option> + </MkSelect> + <MkInput v-model="searchHost" :debounce="true" type="search" style="margin: 0; flex: 1;" :disabled="pagination.params.origin === 'local'"> + <template #label>{{ $ts.host }}</template> + </MkInput> </div> - <div> - <MkAcct v-if="file.user" :user="file.user"/> - <div v-else>{{ $ts.system }}</div> - </div> - <div> - <span style="margin-right: 1em;">{{ file.type }}</span> - <span>{{ bytes(file.size) }}</span> - </div> - <div> - <span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> + <div class="inputs" style="display: flex; padding-top: 1.2em;"> + <MkInput v-model="type" :debounce="true" type="search" style="margin: 0; flex: 1;"> + <template #label>MIME type</template> + </MkInput> </div> + <MkPagination v-slot="{items}" :pagination="pagination" class="urempief" :class="{ grid: viewMode === 'grid' }"> + <button v-for="file in items" :key="file.id" v-tooltip.mfm="`${file.type}\n${bytes(file.size)}\n${new Date(file.createdAt).toLocaleString()}\nby ${file.user ? '@' + Acct.toString(file.user) : 'system'}`" class="file _button" @click="show(file, $event)"> + <MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/> + <div v-if="viewMode === 'list'" class="body"> + <div> + <small style="opacity: 0.7;">{{ file.name }}</small> + </div> + <div> + <MkAcct v-if="file.user" :user="file.user"/> + <div v-else>{{ $ts.system }}</div> + </div> + <div> + <span style="margin-right: 1em;">{{ file.type }}</span> + <span>{{ bytes(file.size) }}</span> + </div> + <div> + <span>{{ $ts.registeredDate }}: <MkTime :time="file.createdAt" mode="detail"/></span> + </div> + </div> + </button> + </MkPagination> </div> - </button> - </MkPagination> - </div> + </div> + </MkSpacer> + </MkStickyContainer> </div> </template> <script lang="ts" setup> import { computed, defineAsyncComponent } from 'vue'; import * as Acct from 'misskey-js/built/acct'; +import XHeader from './_header_.vue'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; @@ -53,8 +61,8 @@ import MkContainer from '@/components/ui/container.vue'; import MkDriveFileThumbnail from '@/components/drive-file-thumbnail.vue'; import bytes from '@/filters/bytes'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let origin = $ref('local'); let type = $ref(null); @@ -82,7 +90,7 @@ function clear() { } function show(file) { - os.pageWindow(`/admin-file/${file.id}`); + os.pageWindow(`/admin/file/${file.id}`); } async function find() { @@ -104,22 +112,23 @@ async function find() { }); } -defineExpose({ - [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.ts.files, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - actions: [{ - text: i18n.ts.lookup, - icon: 'fas fa-search', - handler: find, - }, { - text: i18n.ts.clearCachedFiles, - icon: 'fas fa-trash-alt', - handler: clear, - }], - })), -}); +const headerActions = $computed(() => [{ + text: i18n.ts.lookup, + icon: 'fas fa-search', + handler: find, +}, { + text: i18n.ts.clearCachedFiles, + icon: 'fas fa-trash-alt', + handler: clear, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata(computed(() => ({ + title: i18n.ts.files, + icon: 'fas fa-cloud', + bg: 'var(--bg)', +}))); </script> <style lang="scss" scoped> diff --git a/packages/client/src/pages/admin/index.vue b/packages/client/src/pages/admin/index.vue index 9b7fa5678e..5db91101d7 100644 --- a/packages/client/src/pages/admin/index.vue +++ b/packages/client/src/pages/admin/index.vue @@ -1,8 +1,6 @@ <template> <div ref="el" class="hiyeyicy" :class="{ wide: !narrow }"> - <div v-if="!narrow || initialPage == null" class="nav"> - <MkHeader :info="header"></MkHeader> - + <div v-if="!narrow || initialPage == null" class="nav"> <MkSpacer :content-max="700" :margin-min="16"> <div class="lxpfedzu"> <div class="banner"> @@ -17,29 +15,26 @@ </MkSpacer> </div> <div v-if="!(narrow && initialPage == null)" class="main"> - <MkStickyContainer> - <template #header><MkHeader v-if="childInfo && !childInfo.hideHeader" :info="childInfo"/></template> - <component :is="component" :ref="el => pageChanged(el)" :key="initialPage" v-bind="pageProps"/> - </MkStickyContainer> + <component :is="component" :key="initialPage" v-bind="pageProps"/> </div> </div> </template> <script lang="ts" setup> -import { defineAsyncComponent, nextTick, onMounted, onUnmounted, provide, watch } from 'vue'; +import { defineAsyncComponent, inject, nextTick, onMounted, onUnmounted, provide, watch } from 'vue'; import { i18n } from '@/i18n'; import MkSuperMenu from '@/components/ui/super-menu.vue'; import MkInfo from '@/components/ui/info.vue'; import { scroll } from '@/scripts/scroll'; import { instance } from '@/instance'; -import * as symbols from '@/symbols'; import * as os from '@/os'; import { lookupUser } from '@/scripts/lookup-user'; -import { MisskeyNavigator } from '@/scripts/navigate'; +import { useRouter } from '@/router'; +import { definePageMetadata, provideMetadataReceiver, setPageMetadata } from '@/scripts/page-metadata'; const isEmpty = (x: string | null) => x == null || x === ''; -const nav = new MisskeyNavigator(); +const router = useRouter(); const indexInfo = { title: i18n.ts.controlPanel, @@ -224,7 +219,7 @@ watch(component, () => { watch(() => props.initialPage, () => { if (props.initialPage == null && !narrow) { - nav.push('/admin/overview'); + router.push('/admin/overview'); } else { if (props.initialPage == null) { INFO = indexInfo; @@ -234,7 +229,7 @@ watch(() => props.initialPage, () => { watch(narrow, () => { if (props.initialPage == null && !narrow) { - nav.push('/admin/overview'); + router.push('/admin/overview'); } }); @@ -243,7 +238,7 @@ onMounted(() => { narrow = el.offsetWidth < NARROW_THRESHOLD; if (props.initialPage == null && !narrow) { - nav.push('/admin/overview'); + router.push('/admin/overview'); } }); @@ -251,19 +246,19 @@ onUnmounted(() => { ro.disconnect(); }); -const pageChanged = (page) => { - if (page == null) { +provideMetadataReceiver((info) => { + if (info == null) { childInfo = null; } else { - childInfo = page[symbols.PAGE_INFO]; + childInfo = info; } -}; +}); const invite = () => { os.api('admin/invite').then(x => { os.alert({ type: 'info', - text: x.code + text: x.code, }); }).catch(err => { os.alert({ @@ -279,33 +274,38 @@ const lookup = (ev) => { icon: 'fas fa-user', action: () => { lookupUser(); - } + }, }, { text: i18n.ts.note, icon: 'fas fa-pencil-alt', action: () => { alert('TODO'); - } + }, }, { text: i18n.ts.file, icon: 'fas fa-cloud', action: () => { alert('TODO'); - } + }, }, { text: i18n.ts.instance, icon: 'fas fa-globe', action: () => { alert('TODO'); - } + }, }], ev.currentTarget ?? ev.target); }; +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata(INFO); + defineExpose({ - [symbols.PAGE_INFO]: INFO, header: { title: i18n.ts.controlPanel, - } + }, }); </script> diff --git a/packages/client/src/pages/admin/instance-block.vue b/packages/client/src/pages/admin/instance-block.vue index 3347846a80..1aec151abb 100644 --- a/packages/client/src/pages/admin/instance-block.vue +++ b/packages/client/src/pages/admin/instance-block.vue @@ -1,25 +1,29 @@ <template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> - <FormSuspense :p="init"> - <FormTextarea v-model="blockedHosts" class="_formBlock"> - <span>{{ i18n.ts.blockedInstances }}</span> - <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> - </FormTextarea> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + <FormTextarea v-model="blockedHosts" class="_formBlock"> + <span>{{ i18n.ts.blockedInstances }}</span> + <template #caption>{{ i18n.ts.blockedInstancesDescription }}</template> + </FormTextarea> - <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> - </FormSuspense> -</MkSpacer> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> + </FormSuspense> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import FormButton from '@/components/ui/button.vue'; import FormTextarea from '@/components/form/textarea.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let blockedHosts: string = $ref(''); @@ -36,11 +40,13 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.instanceBlocking, - icon: 'fas fa-ban', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.instanceBlocking, + icon: 'fas fa-ban', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/integrations.vue b/packages/client/src/pages/admin/integrations.vue index d6061d0e51..d407d440b9 100644 --- a/packages/client/src/pages/admin/integrations.vue +++ b/packages/client/src/pages/admin/integrations.vue @@ -1,5 +1,6 @@ -<template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> +<template><MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <FormFolder class="_formBlock"> <template #icon><i class="fab fa-twitter"></i></template> @@ -20,19 +21,19 @@ <XDiscord/> </FormFolder> </FormSuspense> -</MkSpacer> +</MkSpacer></MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; -import FormFolder from '@/components/form/folder.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import XTwitter from './integrations.twitter.vue'; import XGithub from './integrations.github.vue'; import XDiscord from './integrations.discord.vue'; +import FormSuspense from '@/components/form/suspense.vue'; +import FormFolder from '@/components/form/folder.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let enableTwitterIntegration: boolean = $ref(false); let enableGithubIntegration: boolean = $ref(false); @@ -45,11 +46,13 @@ async function init() { enableDiscordIntegration = meta.enableDiscordIntegration; } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.integration, - icon: 'fas fa-share-alt', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.integration, + icon: 'fas fa-share-alt', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/object-storage.vue b/packages/client/src/pages/admin/object-storage.vue index d109db9c38..bae5277f49 100644 --- a/packages/client/src/pages/admin/object-storage.vue +++ b/packages/client/src/pages/admin/object-storage.vue @@ -1,72 +1,76 @@ <template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> - <FormSuspense :p="init"> - <div class="_formRoot"> - <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + <div class="_formRoot"> + <FormSwitch v-model="useObjectStorage" class="_formBlock">{{ i18n.ts.useObjectStorage }}</FormSwitch> - <template v-if="useObjectStorage"> - <FormInput v-model="objectStorageBaseUrl" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> - <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> - </FormInput> - - <FormInput v-model="objectStorageBucket" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageBucket }}</template> - <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template> - </FormInput> - - <FormInput v-model="objectStoragePrefix" class="_formBlock"> - <template #label>{{ i18n.ts.objectStoragePrefix }}</template> - <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> - </FormInput> + <template v-if="useObjectStorage"> + <FormInput v-model="objectStorageBaseUrl" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageBaseUrl }}</template> + <template #caption>{{ i18n.ts.objectStorageBaseUrlDesc }}</template> + </FormInput> - <FormInput v-model="objectStorageEndpoint" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageEndpoint }}</template> - <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> - </FormInput> + <FormInput v-model="objectStorageBucket" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageBucket }}</template> + <template #caption>{{ i18n.ts.objectStorageBucketDesc }}</template> + </FormInput> - <FormInput v-model="objectStorageRegion" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageRegion }}</template> - <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> - </FormInput> + <FormInput v-model="objectStoragePrefix" class="_formBlock"> + <template #label>{{ i18n.ts.objectStoragePrefix }}</template> + <template #caption>{{ i18n.ts.objectStoragePrefixDesc }}</template> + </FormInput> - <FormSplit :min-width="280"> - <FormInput v-model="objectStorageAccessKey" class="_formBlock"> - <template #prefix><i class="fas fa-key"></i></template> - <template #label>Access key</template> + <FormInput v-model="objectStorageEndpoint" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageEndpoint }}</template> + <template #caption>{{ i18n.ts.objectStorageEndpointDesc }}</template> </FormInput> - <FormInput v-model="objectStorageSecretKey" class="_formBlock"> - <template #prefix><i class="fas fa-key"></i></template> - <template #label>Secret key</template> + <FormInput v-model="objectStorageRegion" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageRegion }}</template> + <template #caption>{{ i18n.ts.objectStorageRegionDesc }}</template> </FormInput> - </FormSplit> - <FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageUseSSL }}</template> - <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> - </FormSwitch> + <FormSplit :min-width="280"> + <FormInput v-model="objectStorageAccessKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Access key</template> + </FormInput> - <FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageUseProxy }}</template> - <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> - </FormSwitch> + <FormInput v-model="objectStorageSecretKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Secret key</template> + </FormInput> + </FormSplit> - <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> - <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> - </FormSwitch> + <FormSwitch v-model="objectStorageUseSSL" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageUseSSL }}</template> + <template #caption>{{ i18n.ts.objectStorageUseSSLDesc }}</template> + </FormSwitch> - <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> - <template #label>s3ForcePathStyle</template> - </FormSwitch> - </template> - </div> - </FormSuspense> -</MkSpacer> + <FormSwitch v-model="objectStorageUseProxy" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageUseProxy }}</template> + <template #caption>{{ i18n.ts.objectStorageUseProxyDesc }}</template> + </FormSwitch> + + <FormSwitch v-model="objectStorageSetPublicRead" class="_formBlock"> + <template #label>{{ i18n.ts.objectStorageSetPublicRead }}</template> + </FormSwitch> + + <FormSwitch v-model="objectStorageS3ForcePathStyle" class="_formBlock"> + <template #label>s3ForcePathStyle</template> + </FormSwitch> + </template> + </div> + </FormSuspense> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormGroup from '@/components/form/group.vue'; @@ -74,9 +78,9 @@ import FormSuspense from '@/components/form/suspense.vue'; import FormSplit from '@/components/form/split.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let useObjectStorage: boolean = $ref(false); let objectStorageBaseUrl: string | null = $ref(null); @@ -129,17 +133,18 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.objectStorage, - icon: 'fas fa-cloud', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-check', - text: i18n.ts.save, - handler: save, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.objectStorage, + icon: 'fas fa-cloud', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/other-settings.vue b/packages/client/src/pages/admin/other-settings.vue index 552b05f347..59b3503c3c 100644 --- a/packages/client/src/pages/admin/other-settings.vue +++ b/packages/client/src/pages/admin/other-settings.vue @@ -1,18 +1,22 @@ <template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> - <FormSuspense :p="init"> - none - </FormSuspense> -</MkSpacer> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + none + </FormSuspense> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; async function init() { await os.api('admin/meta'); @@ -24,17 +28,18 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.other, - icon: 'fas fa-cogs', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-check', - text: i18n.ts.save, - handler: save, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.other, + icon: 'fas fa-cogs', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/overview.vue b/packages/client/src/pages/admin/overview.vue index cc69424c3b..82b3c33852 100644 --- a/packages/client/src/pages/admin/overview.vue +++ b/packages/client/src/pages/admin/overview.vue @@ -35,7 +35,7 @@ </MkContainer> </div> - <!--<XMetrics/>--> + <!--<XMetrics/>--> <MkFolder style="margin: var(--margin)"> <template #header><i class="fas fa-info-circle"></i> {{ i18n.ts.info }}</template> @@ -67,6 +67,7 @@ <script lang="ts" setup> import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue'; +import XMetrics from './metrics.vue'; import MkInstanceStats from '@/components/instance-stats.vue'; import MkNumberDiff from '@/components/number-diff.vue'; import MkContainer from '@/components/ui/container.vue'; @@ -74,11 +75,10 @@ import MkFolder from '@/components/ui/folder.vue'; import MkQueueChart from '@/components/queue-chart.vue'; import { version, url } from '@/config'; import number from '@/filters/number'; -import XMetrics from './metrics.vue'; import * as os from '@/os'; import { stream } from '@/stream'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let stats: any = $ref(null); let serverInfo: any = $ref(null); @@ -106,7 +106,7 @@ onMounted(async () => { nextTick(() => { queueStatsConnection.send('requestLog', { id: Math.random().toString().substr(2, 8), - length: 200 + length: 200, }); }); }); @@ -115,12 +115,14 @@ onBeforeUnmount(() => { queueStatsConnection.dispose(); }); -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.dashboard, - icon: 'fas fa-tachometer-alt', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.dashboard, + icon: 'fas fa-tachometer-alt', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/proxy-account.vue b/packages/client/src/pages/admin/proxy-account.vue index 727e20e7e5..0c5bb1bc9f 100644 --- a/packages/client/src/pages/admin/proxy-account.vue +++ b/packages/client/src/pages/admin/proxy-account.vue @@ -1,5 +1,6 @@ -<template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> +<template><MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> <FormSuspense :p="init"> <MkInfo class="_formBlock">{{ i18n.ts.proxyAccountDescription }}</MkInfo> <MkKeyValue class="_formBlock"> @@ -9,7 +10,7 @@ <FormButton primary class="_formBlock" @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</FormButton> </FormSuspense> -</MkSpacer> +</MkSpacer></MkStickyContainer> </template> <script lang="ts" setup> @@ -19,9 +20,9 @@ import FormButton from '@/components/ui/button.vue'; import MkInfo from '@/components/ui/info.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let proxyAccount: any = $ref(null); let proxyAccountId: any = $ref(null); @@ -50,11 +51,13 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.proxyAccount, - icon: 'fas fa-ghost', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.proxyAccount, + icon: 'fas fa-ghost', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/queue.vue b/packages/client/src/pages/admin/queue.vue index 656b18199f..c2865525ab 100644 --- a/packages/client/src/pages/admin/queue.vue +++ b/packages/client/src/pages/admin/queue.vue @@ -1,24 +1,28 @@ <template> -<MkSpacer :content-max="800"> - <XQueue :connection="connection" domain="inbox"> - <template #title>In</template> - </XQueue> - <XQueue :connection="connection" domain="deliver"> - <template #title>Out</template> - </XQueue> - <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> -</MkSpacer> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="800"> + <XQueue :connection="connection" domain="inbox"> + <template #title>In</template> + </XQueue> + <XQueue :connection="connection" domain="deliver"> + <template #title>Out</template> + </XQueue> + <MkButton danger @click="clear()"><i class="fas fa-trash-alt"></i> {{ i18n.ts.clearQueue }}</MkButton> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { markRaw, onMounted, onBeforeUnmount, nextTick } from 'vue'; -import MkButton from '@/components/ui/button.vue'; import XQueue from './queue.chart.vue'; +import XHeader from './_header_.vue'; +import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import { stream } from '@/stream'; -import * as symbols from '@/symbols'; import * as config from '@/config'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; const connection = markRaw(stream.useChannel('queueStats')); @@ -38,7 +42,7 @@ onMounted(() => { nextTick(() => { connection.send('requestLog', { id: Math.random().toString().substr(2, 8), - length: 200 + length: 200, }); }); }); @@ -47,19 +51,20 @@ onBeforeUnmount(() => { connection.dispose(); }); -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.jobQueue, - icon: 'fas fa-clipboard-list', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-up-right-from-square', - text: i18n.ts.dashboard, - handler: () => { - window.open(config.url + '/queue', '_blank'); - }, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-up-right-from-square', + text: i18n.ts.dashboard, + handler: () => { + window.open(config.url + '/queue', '_blank'); + }, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.jobQueue, + icon: 'fas fa-clipboard-list', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/relays.vue b/packages/client/src/pages/admin/relays.vue index 1a36bb4753..1ca4f2df09 100644 --- a/packages/client/src/pages/admin/relays.vue +++ b/packages/client/src/pages/admin/relays.vue @@ -1,24 +1,28 @@ <template> -<MkSpacer :content-max="800"> - <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> - <div>{{ relay.inbox }}</div> - <div class="status"> - <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i> - <i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i> - <i v-else class="fas fa-clock icon requesting"></i> - <span>{{ $t(`_relayStatus.${relay.status}`) }}</span> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="800"> + <div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel _block" style="padding: 16px;"> + <div>{{ relay.inbox }}</div> + <div class="status"> + <i v-if="relay.status === 'accepted'" class="fas fa-check icon accepted"></i> + <i v-else-if="relay.status === 'rejected'" class="fas fa-ban icon rejected"></i> + <i v-else class="fas fa-clock icon requesting"></i> + <span>{{ $t(`_relayStatus.${relay.status}`) }}</span> + </div> + <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> </div> - <MkButton class="button" inline danger @click="remove(relay.inbox)"><i class="fas fa-trash-alt"></i> {{ i18n.ts.remove }}</MkButton> - </div> -</MkSpacer> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let relays: any[] = $ref([]); @@ -26,30 +30,30 @@ async function addRelay() { const { canceled, result: inbox } = await os.inputText({ title: i18n.ts.addRelay, type: 'url', - placeholder: i18n.ts.inboxUrl + placeholder: i18n.ts.inboxUrl, }); if (canceled) return; os.api('admin/relays/add', { - inbox + inbox, }).then((relay: any) => { refresh(); }).catch((err: any) => { os.alert({ type: 'error', - text: err.message || err + text: err.message || err, }); }); } function remove(inbox: string) { os.api('admin/relays/remove', { - inbox + inbox, }).then(() => { refresh(); }).catch((err: any) => { os.alert({ type: 'error', - text: err.message || err + text: err.message || err, }); }); } @@ -62,18 +66,19 @@ function refresh() { refresh(); -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.relays, - icon: 'fas fa-globe', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-plus', - text: i18n.ts.addRelay, - handler: addRelay, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.addRelay, + handler: addRelay, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.relays, + icon: 'fas fa-globe', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/security.vue b/packages/client/src/pages/admin/security.vue index 6b8f70cca5..65b08565cd 100644 --- a/packages/client/src/pages/admin/security.vue +++ b/packages/client/src/pages/admin/security.vue @@ -1,36 +1,41 @@ <template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> - <FormSuspense :p="init"> - <div class="_formRoot"> - <FormFolder class="_formBlock"> - <template #icon><i class="fas fa-shield-alt"></i></template> - <template #label>{{ i18n.ts.botProtection }}</template> - <template v-if="enableHcaptcha" #suffix>hCaptcha</template> - <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> - <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> +<MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + <div class="_formRoot"> + <FormFolder class="_formBlock"> + <template #icon><i class="fas fa-shield-alt"></i></template> + <template #label>{{ i18n.ts.botProtection }}</template> + <template v-if="enableHcaptcha" #suffix>hCaptcha</template> + <template v-else-if="enableRecaptcha" #suffix>reCAPTCHA</template> + <template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template> - <XBotProtection/> - </FormFolder> + <XBotProtection/> + </FormFolder> - <FormFolder class="_formBlock"> - <template #label>Summaly Proxy</template> + <FormFolder class="_formBlock"> + <template #label>Summaly Proxy</template> - <div class="_formRoot"> - <FormInput v-model="summalyProxy" class="_formBlock"> - <template #prefix><i class="fas fa-link"></i></template> - <template #label>Summaly Proxy URL</template> - </FormInput> + <div class="_formRoot"> + <FormInput v-model="summalyProxy" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>Summaly Proxy URL</template> + </FormInput> - <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> - </div> - </FormFolder> - </div> - </FormSuspense> -</MkSpacer> + <FormButton primary class="_formBlock" @click="save"><i class="fas fa-save"></i> {{ i18n.ts.save }}</FormButton> + </div> + </FormFolder> + </div> + </FormSuspense> + </MkSpacer> +</MkStickyContainer> </template> <script lang="ts" setup> import { } from 'vue'; +import XBotProtection from './bot-protection.vue'; +import XHeader from './_header_.vue'; import FormFolder from '@/components/form/folder.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInfo from '@/components/ui/info.vue'; @@ -38,11 +43,10 @@ import FormSuspense from '@/components/form/suspense.vue'; import FormSection from '@/components/form/section.vue'; import FormInput from '@/components/form/input.vue'; import FormButton from '@/components/ui/button.vue'; -import XBotProtection from './bot-protection.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let summalyProxy: string = $ref(''); let enableHcaptcha: boolean = $ref(false); @@ -63,11 +67,13 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.security, - icon: 'fas fa-lock', - bg: 'var(--bg)', - } +const headerActions = $computed(() => []); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.security, + icon: 'fas fa-lock', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/settings.vue b/packages/client/src/pages/admin/settings.vue index 6dc30fe50b..a5767cc2c2 100644 --- a/packages/client/src/pages/admin/settings.vue +++ b/packages/client/src/pages/admin/settings.vue @@ -1,149 +1,155 @@ <template> -<MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> - <FormSuspense :p="init"> - <div class="_formRoot"> - <FormInput v-model="name" class="_formBlock"> - <template #label>{{ i18n.ts.instanceName }}</template> - </FormInput> +<div> + <MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="700" :margin-min="16" :margin-max="32"> + <FormSuspense :p="init"> + <div class="_formRoot"> + <FormInput v-model="name" class="_formBlock"> + <template #label>{{ i18n.ts.instanceName }}</template> + </FormInput> - <FormTextarea v-model="description" class="_formBlock"> - <template #label>{{ i18n.ts.instanceDescription }}</template> - </FormTextarea> + <FormTextarea v-model="description" class="_formBlock"> + <template #label>{{ i18n.ts.instanceDescription }}</template> + </FormTextarea> - <FormInput v-model="tosUrl" class="_formBlock"> - <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ i18n.ts.tosUrl }}</template> - </FormInput> + <FormInput v-model="tosUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ i18n.ts.tosUrl }}</template> + </FormInput> - <FormSplit :min-width="300"> - <FormInput v-model="maintainerName" class="_formBlock"> - <template #label>{{ i18n.ts.maintainerName }}</template> - </FormInput> + <FormSplit :min-width="300"> + <FormInput v-model="maintainerName" class="_formBlock"> + <template #label>{{ i18n.ts.maintainerName }}</template> + </FormInput> - <FormInput v-model="maintainerEmail" type="email" class="_formBlock"> - <template #prefix><i class="fas fa-envelope"></i></template> - <template #label>{{ i18n.ts.maintainerEmail }}</template> - </FormInput> - </FormSplit> + <FormInput v-model="maintainerEmail" type="email" class="_formBlock"> + <template #prefix><i class="fas fa-envelope"></i></template> + <template #label>{{ i18n.ts.maintainerEmail }}</template> + </FormInput> + </FormSplit> - <FormTextarea v-model="pinnedUsers" class="_formBlock"> - <template #label>{{ i18n.ts.pinnedUsers }}</template> - <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> - </FormTextarea> + <FormTextarea v-model="pinnedUsers" class="_formBlock"> + <template #label>{{ i18n.ts.pinnedUsers }}</template> + <template #caption>{{ i18n.ts.pinnedUsersDescription }}</template> + </FormTextarea> - <FormSection> - <FormSwitch v-model="enableRegistration" class="_formBlock"> - <template #label>{{ i18n.ts.enableRegistration }}</template> - </FormSwitch> + <FormSection> + <FormSwitch v-model="enableRegistration" class="_formBlock"> + <template #label>{{ i18n.ts.enableRegistration }}</template> + </FormSwitch> - <FormSwitch v-model="emailRequiredForSignup" class="_formBlock"> - <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> - </FormSwitch> - </FormSection> + <FormSwitch v-model="emailRequiredForSignup" class="_formBlock"> + <template #label>{{ i18n.ts.emailRequiredForSignup }}</template> + </FormSwitch> + </FormSection> - <FormSection> - <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch> - <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch> - <FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo> - </FormSection> + <FormSection> + <FormSwitch v-model="enableLocalTimeline" class="_formBlock">{{ i18n.ts.enableLocalTimeline }}</FormSwitch> + <FormSwitch v-model="enableGlobalTimeline" class="_formBlock">{{ i18n.ts.enableGlobalTimeline }}</FormSwitch> + <FormInfo class="_formBlock">{{ i18n.ts.disablingTimelinesInfo }}</FormInfo> + </FormSection> - <FormSection> - <template #label>{{ i18n.ts.theme }}</template> + <FormSection> + <template #label>{{ i18n.ts.theme }}</template> - <FormInput v-model="iconUrl" class="_formBlock"> - <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ i18n.ts.iconUrl }}</template> - </FormInput> + <FormInput v-model="iconUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ i18n.ts.iconUrl }}</template> + </FormInput> - <FormInput v-model="bannerUrl" class="_formBlock"> - <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ i18n.ts.bannerUrl }}</template> - </FormInput> + <FormInput v-model="bannerUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ i18n.ts.bannerUrl }}</template> + </FormInput> - <FormInput v-model="backgroundImageUrl" class="_formBlock"> - <template #prefix><i class="fas fa-link"></i></template> - <template #label>{{ i18n.ts.backgroundImageUrl }}</template> - </FormInput> + <FormInput v-model="backgroundImageUrl" class="_formBlock"> + <template #prefix><i class="fas fa-link"></i></template> + <template #label>{{ i18n.ts.backgroundImageUrl }}</template> + </FormInput> - <FormInput v-model="themeColor" class="_formBlock"> - <template #prefix><i class="fas fa-palette"></i></template> - <template #label>{{ i18n.ts.themeColor }}</template> - <template #caption>#RRGGBB</template> - </FormInput> + <FormInput v-model="themeColor" class="_formBlock"> + <template #prefix><i class="fas fa-palette"></i></template> + <template #label>{{ i18n.ts.themeColor }}</template> + <template #caption>#RRGGBB</template> + </FormInput> - <FormTextarea v-model="defaultLightTheme" class="_formBlock"> - <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> - <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> - </FormTextarea> + <FormTextarea v-model="defaultLightTheme" class="_formBlock"> + <template #label>{{ i18n.ts.instanceDefaultLightTheme }}</template> + <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> + </FormTextarea> - <FormTextarea v-model="defaultDarkTheme" class="_formBlock"> - <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> - <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> - </FormTextarea> - </FormSection> + <FormTextarea v-model="defaultDarkTheme" class="_formBlock"> + <template #label>{{ i18n.ts.instanceDefaultDarkTheme }}</template> + <template #caption>{{ i18n.ts.instanceDefaultThemeDescription }}</template> + </FormTextarea> + </FormSection> - <FormSection> - <template #label>{{ i18n.ts.files }}</template> + <FormSection> + <template #label>{{ i18n.ts.files }}</template> - <FormSwitch v-model="cacheRemoteFiles" class="_formBlock"> - <template #label>{{ i18n.ts.cacheRemoteFiles }}</template> - <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> - </FormSwitch> + <FormSwitch v-model="cacheRemoteFiles" class="_formBlock"> + <template #label>{{ i18n.ts.cacheRemoteFiles }}</template> + <template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template> + </FormSwitch> - <FormSplit :min-width="280"> - <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock"> - <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template> - <template #suffix>MB</template> - <template #caption>{{ i18n.ts.inMb }}</template> - </FormInput> + <FormSplit :min-width="280"> + <FormInput v-model="localDriveCapacityMb" type="number" class="_formBlock"> + <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template> + <template #suffix>MB</template> + <template #caption>{{ i18n.ts.inMb }}</template> + </FormInput> - <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> - <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template> - <template #suffix>MB</template> - <template #caption>{{ i18n.ts.inMb }}</template> - </FormInput> - </FormSplit> - </FormSection> + <FormInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles" class="_formBlock"> + <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template> + <template #suffix>MB</template> + <template #caption>{{ i18n.ts.inMb }}</template> + </FormInput> + </FormSplit> + </FormSection> - <FormSection> - <template #label>ServiceWorker</template> + <FormSection> + <template #label>ServiceWorker</template> - <FormSwitch v-model="enableServiceWorker" class="_formBlock"> - <template #label>{{ i18n.ts.enableServiceworker }}</template> - <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> - </FormSwitch> + <FormSwitch v-model="enableServiceWorker" class="_formBlock"> + <template #label>{{ i18n.ts.enableServiceworker }}</template> + <template #caption>{{ i18n.ts.serviceworkerInfo }}</template> + </FormSwitch> - <template v-if="enableServiceWorker"> - <FormInput v-model="swPublicKey" class="_formBlock"> - <template #prefix><i class="fas fa-key"></i></template> - <template #label>Public key</template> - </FormInput> + <template v-if="enableServiceWorker"> + <FormInput v-model="swPublicKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Public key</template> + </FormInput> - <FormInput v-model="swPrivateKey" class="_formBlock"> - <template #prefix><i class="fas fa-key"></i></template> - <template #label>Private key</template> - </FormInput> - </template> - </FormSection> + <FormInput v-model="swPrivateKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>Private key</template> + </FormInput> + </template> + </FormSection> - <FormSection> - <template #label>DeepL Translation</template> + <FormSection> + <template #label>DeepL Translation</template> - <FormInput v-model="deeplAuthKey" class="_formBlock"> - <template #prefix><i class="fas fa-key"></i></template> - <template #label>DeepL Auth Key</template> - </FormInput> - <FormSwitch v-model="deeplIsPro" class="_formBlock"> - <template #label>Pro account</template> - </FormSwitch> - </FormSection> - </div> - </FormSuspense> -</MkSpacer> + <FormInput v-model="deeplAuthKey" class="_formBlock"> + <template #prefix><i class="fas fa-key"></i></template> + <template #label>DeepL Auth Key</template> + </FormInput> + <FormSwitch v-model="deeplIsPro" class="_formBlock"> + <template #label>Pro account</template> + </FormSwitch> + </FormSection> + </div> + </FormSuspense> + </MkSpacer> + </MkStickyContainer> +</div> </template> <script lang="ts" setup> import { } from 'vue'; +import XHeader from './_header_.vue'; import FormSwitch from '@/components/form/switch.vue'; import FormInput from '@/components/form/input.vue'; import FormTextarea from '@/components/form/textarea.vue'; @@ -152,9 +158,9 @@ import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { fetchInstance } from '@/instance'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let name: string | null = $ref(null); let description: string | null = $ref(null); @@ -240,17 +246,18 @@ function save() { }); } -defineExpose({ - [symbols.PAGE_INFO]: { - title: i18n.ts.general, - icon: 'fas fa-cog', - bg: 'var(--bg)', - actions: [{ - asFullButton: true, - icon: 'fas fa-check', - text: i18n.ts.save, - handler: save, - }], - } +const headerActions = $computed(() => [{ + asFullButton: true, + icon: 'fas fa-check', + text: i18n.ts.save, + handler: save, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata({ + title: i18n.ts.general, + icon: 'fas fa-cog', + bg: 'var(--bg)', }); </script> diff --git a/packages/client/src/pages/admin/users.vue b/packages/client/src/pages/admin/users.vue index f05aa5ff45..dccf952ba9 100644 --- a/packages/client/src/pages/admin/users.vue +++ b/packages/client/src/pages/admin/users.vue @@ -1,76 +1,84 @@ <template> -<div class="lknzcolw"> - <div class="users"> - <div class="inputs"> - <MkSelect v-model="sort" style="flex: 1;"> - <template #label>{{ $ts.sort }}</template> - <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> - <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> - <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> - <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> - </MkSelect> - <MkSelect v-model="state" style="flex: 1;"> - <template #label>{{ $ts.state }}</template> - <option value="all">{{ $ts.all }}</option> - <option value="available">{{ $ts.normal }}</option> - <option value="admin">{{ $ts.administrator }}</option> - <option value="moderator">{{ $ts.moderator }}</option> - <option value="silenced">{{ $ts.silence }}</option> - <option value="suspended">{{ $ts.suspend }}</option> - </MkSelect> - <MkSelect v-model="origin" style="flex: 1;"> - <template #label>{{ $ts.instance }}</template> - <option value="combined">{{ $ts.all }}</option> - <option value="local">{{ $ts.local }}</option> - <option value="remote">{{ $ts.remote }}</option> - </MkSelect> - </div> - <div class="inputs"> - <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> - <template #prefix>@</template> - <template #label>{{ $ts.username }}</template> - </MkInput> - <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> - <template #prefix>@</template> - <template #label>{{ $ts.host }}</template> - </MkInput> - </div> - - <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> - <button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)"> - <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> - <div class="body"> - <header> - <MkUserName class="name" :user="user"/> - <span class="acct">@{{ acct(user) }}</span> - <span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span> - <span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span> - <span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span> - <span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span> - </header> - <div> - <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> +<div> + <MkStickyContainer> + <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :content-max="900"> + <div class="lknzcolw"> + <div class="users"> + <div class="inputs"> + <MkSelect v-model="sort" style="flex: 1;"> + <template #label>{{ $ts.sort }}</template> + <option value="-createdAt">{{ $ts.registeredDate }} ({{ $ts.ascendingOrder }})</option> + <option value="+createdAt">{{ $ts.registeredDate }} ({{ $ts.descendingOrder }})</option> + <option value="-updatedAt">{{ $ts.lastUsed }} ({{ $ts.ascendingOrder }})</option> + <option value="+updatedAt">{{ $ts.lastUsed }} ({{ $ts.descendingOrder }})</option> + </MkSelect> + <MkSelect v-model="state" style="flex: 1;"> + <template #label>{{ $ts.state }}</template> + <option value="all">{{ $ts.all }}</option> + <option value="available">{{ $ts.normal }}</option> + <option value="admin">{{ $ts.administrator }}</option> + <option value="moderator">{{ $ts.moderator }}</option> + <option value="silenced">{{ $ts.silence }}</option> + <option value="suspended">{{ $ts.suspend }}</option> + </MkSelect> + <MkSelect v-model="origin" style="flex: 1;"> + <template #label>{{ $ts.instance }}</template> + <option value="combined">{{ $ts.all }}</option> + <option value="local">{{ $ts.local }}</option> + <option value="remote">{{ $ts.remote }}</option> + </MkSelect> </div> - <div> - <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> + <div class="inputs"> + <MkInput v-model="searchUsername" style="flex: 1;" type="text" spellcheck="false" @update:modelValue="$refs.users.reload()"> + <template #prefix>@</template> + <template #label>{{ $ts.username }}</template> + </MkInput> + <MkInput v-model="searchHost" style="flex: 1;" type="text" spellcheck="false" :disabled="pagination.params.origin === 'local'" @update:modelValue="$refs.users.reload()"> + <template #prefix>@</template> + <template #label>{{ $ts.host }}</template> + </MkInput> </div> + + <MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users"> + <button v-for="user in items" :key="user.id" class="user _panel _button _gap" @click="show(user)"> + <MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/> + <div class="body"> + <header> + <MkUserName class="name" :user="user"/> + <span class="acct">@{{ acct(user) }}</span> + <span v-if="user.isAdmin" class="staff"><i class="fas fa-bookmark"></i></span> + <span v-if="user.isModerator" class="staff"><i class="far fa-bookmark"></i></span> + <span v-if="user.isSilenced" class="punished"><i class="fas fa-microphone-slash"></i></span> + <span v-if="user.isSuspended" class="punished"><i class="fas fa-snowflake"></i></span> + </header> + <div> + <span>{{ $ts.lastUsed }}: <MkTime v-if="user.updatedAt" :time="user.updatedAt" mode="detail"/></span> + </div> + <div> + <span>{{ $ts.registeredDate }}: <MkTime :time="user.createdAt" mode="detail"/></span> + </div> + </div> + </button> + </MkPagination> </div> - </button> - </MkPagination> - </div> + </div> + </MkSpacer> + </MkStickyContainer> </div> </template> <script lang="ts" setup> import { computed } from 'vue'; +import XHeader from './_header_.vue'; import MkInput from '@/components/form/input.vue'; import MkSelect from '@/components/form/select.vue'; import MkPagination from '@/components/ui/pagination.vue'; import { acct } from '@/filters/user'; import * as os from '@/os'; -import * as symbols from '@/symbols'; import { lookupUser } from '@/scripts/lookup-user'; import { i18n } from '@/i18n'; +import { definePageMetadata } from '@/scripts/page-metadata'; let paginationComponent = $ref<InstanceType<typeof MkPagination>>(); @@ -89,7 +97,7 @@ const pagination = { username: searchUsername, hostname: searchHost, })), - offsetMode: true + offsetMode: true, }; function searchUser() { @@ -106,7 +114,7 @@ async function addUser() { const { canceled: canceled2, result: password } = await os.inputText({ title: i18n.ts.password, - type: 'password' + type: 'password', }); if (canceled2) return; @@ -122,34 +130,34 @@ function show(user) { os.pageWindow(`/user-info/${user.id}`); } -defineExpose({ - [symbols.PAGE_INFO]: computed(() => ({ - title: i18n.ts.users, - icon: 'fas fa-users', - bg: 'var(--bg)', - actions: [{ - icon: 'fas fa-search', - text: i18n.ts.search, - handler: searchUser - }, { - asFullButton: true, - icon: 'fas fa-plus', - text: i18n.ts.addUser, - handler: addUser - }, { - asFullButton: true, - icon: 'fas fa-search', - text: i18n.ts.lookup, - handler: lookupUser - }], - })), -}); +const headerActions = $computed(() => [{ + icon: 'fas fa-search', + text: i18n.ts.search, + handler: searchUser, +}, { + asFullButton: true, + icon: 'fas fa-plus', + text: i18n.ts.addUser, + handler: addUser, +}, { + asFullButton: true, + icon: 'fas fa-search', + text: i18n.ts.lookup, + handler: lookupUser, +}]); + +const headerTabs = $computed(() => []); + +definePageMetadata(computed(() => ({ + title: i18n.ts.users, + icon: 'fas fa-users', + bg: 'var(--bg)', +}))); </script> <style lang="scss" scoped> .lknzcolw { > .users { - margin: var(--margin); > .inputs { display: flex; |