feat(client): registry editor
This commit is contained in:
		
							
								
								
									
										96
									
								
								packages/client/src/pages/registry.keys.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								packages/client/src/pages/registry.keys.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="600"> | ||||
| 		<FormSplit> | ||||
| 			<MkKeyValue class="_formBlock"> | ||||
| 				<template #key>{{ $ts._registry.domain }}</template> | ||||
| 				<template #value>{{ $ts.system }}</template> | ||||
| 			</MkKeyValue> | ||||
| 			<MkKeyValue class="_formBlock"> | ||||
| 				<template #key>{{ $ts._registry.scope }}</template> | ||||
| 				<template #value>{{ scope.join('/') }}</template> | ||||
| 			</MkKeyValue> | ||||
| 		</FormSplit> | ||||
| 		 | ||||
| 		<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> | ||||
|  | ||||
| 		<FormSection v-if="keys"> | ||||
| 			<template #label>{{ i18n.ts.keys }}</template> | ||||
| 			<div class="_formLinks"> | ||||
| 				<FormLink v-for="key in keys" :to="`/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink> | ||||
| 			</div> | ||||
| 		</FormSection> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, watch } from 'vue'; | ||||
| import JSON5 from 'json5'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import FormSplit from '@/components/form/split.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	path: string; | ||||
| }>(); | ||||
|  | ||||
| const scope = $computed(() => props.path.split('/')); | ||||
|  | ||||
| let keys = $ref(null); | ||||
|  | ||||
| function fetchKeys() { | ||||
| 	os.api('i/registry/keys-with-type', { | ||||
| 		scope: scope, | ||||
| 	}).then(res => { | ||||
| 		keys = Object.entries(res).sort((a, b) => a[0].localeCompare(b[0])); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| async function createKey() { | ||||
| 	const { canceled, result } = await os.form(i18n.ts._registry.createKey, { | ||||
| 		key: { | ||||
| 			type: 'string', | ||||
| 			label: i18n.ts._registry.key, | ||||
| 		}, | ||||
| 		value: { | ||||
| 			type: 'string', | ||||
| 			multiline: true, | ||||
| 			label: i18n.ts.value, | ||||
| 		}, | ||||
| 		scope: { | ||||
| 			type: 'string', | ||||
| 			label: i18n.ts._registry.scope, | ||||
| 			default: scope.join('/'), | ||||
| 		}, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	os.apiWithDialog('i/registry/set', { | ||||
| 		scope: result.scope.split('/'), | ||||
| 		key: result.key, | ||||
| 		value: JSON5.parse(result.value), | ||||
| 	}).then(() => { | ||||
| 		fetchKeys(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| watch(() => props.path, fetchKeys, { immediate: true }); | ||||
|  | ||||
| const headerActions = $computed(() => []); | ||||
|  | ||||
| const headerTabs = $computed(() => []); | ||||
|  | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.registry, | ||||
| 	icon: 'fas fa-cogs', | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
							
								
								
									
										123
									
								
								packages/client/src/pages/registry.value.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								packages/client/src/pages/registry.value.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="600"> | ||||
| 		<FormInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</FormInfo> | ||||
|  | ||||
| 		<template v-if="value"> | ||||
| 			<FormSplit> | ||||
| 				<MkKeyValue class="_formBlock"> | ||||
| 					<template #key>{{ $ts._registry.domain }}</template> | ||||
| 					<template #value>{{ $ts.system }}</template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue class="_formBlock"> | ||||
| 					<template #key>{{ $ts._registry.scope }}</template> | ||||
| 					<template #value>{{ scope.join('/') }}</template> | ||||
| 				</MkKeyValue> | ||||
| 				<MkKeyValue class="_formBlock"> | ||||
| 					<template #key>{{ $ts._registry.key }}</template> | ||||
| 					<template #value>{{ key }}</template> | ||||
| 				</MkKeyValue> | ||||
| 			</FormSplit> | ||||
| 			 | ||||
| 			<FormTextarea v-model="valueForEditor" tall class="_formBlock _monospace"> | ||||
| 				<template #label>{{ $ts.value }} (JSON)</template> | ||||
| 			</FormTextarea> | ||||
|  | ||||
| 			<MkButton class="_formBlock" primary @click="save"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> | ||||
|  | ||||
| 			<MkKeyValue class="_formBlock"> | ||||
| 				<template #key>{{ $ts.updatedAt }}</template> | ||||
| 				<template #value><MkTime :time="value.updatedAt" mode="detail"/></template> | ||||
| 			</MkKeyValue> | ||||
|  | ||||
| 			<MkButton danger @click="del"><i class="fas fa-trash"></i> {{ $ts.delete }}</MkButton> | ||||
| 		</template> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, watch } from 'vue'; | ||||
| import JSON5 from 'json5'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import MkKeyValue from '@/components/key-value.vue'; | ||||
| import FormTextarea from '@/components/form/textarea.vue'; | ||||
| import FormSplit from '@/components/form/split.vue'; | ||||
| import FormInfo from '@/components/ui/info.vue'; | ||||
|  | ||||
| const props = defineProps<{ | ||||
| 	path: string; | ||||
| }>(); | ||||
|  | ||||
| const scope = $computed(() => props.path.split('/').slice(0, -1)); | ||||
| const key = $computed(() => props.path.split('/').at(-1)); | ||||
|  | ||||
| let value = $ref(null); | ||||
| let valueForEditor = $ref(null); | ||||
|  | ||||
| function fetchValue() { | ||||
| 	os.api('i/registry/get-detail', { | ||||
| 		scope, | ||||
| 		key, | ||||
| 	}).then(res => { | ||||
| 		value = res; | ||||
| 		valueForEditor = JSON5.stringify(res.value, null, '\t'); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| async function save() { | ||||
| 	try { | ||||
| 		JSON5.parse(valueForEditor); | ||||
| 	} catch (e) { | ||||
| 		os.alert({ | ||||
| 			type: 'error', | ||||
| 			text: i18n.ts.invalidValue, | ||||
| 		}); | ||||
| 		return; | ||||
| 	} | ||||
| 	os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.ts.saveConfirm, | ||||
| 	}).then(({ canceled }) => { | ||||
| 		if (canceled) return; | ||||
| 		os.apiWithDialog('i/registry/set', { | ||||
| 			scope, | ||||
| 			key, | ||||
| 			value: JSON5.parse(valueForEditor), | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| function del() { | ||||
| 	os.confirm({ | ||||
| 		type: 'warning', | ||||
| 		text: i18n.ts.deleteConfirm, | ||||
| 	}).then(({ canceled }) => { | ||||
| 		if (canceled) return; | ||||
| 		os.apiWithDialog('i/registry/remove', { | ||||
| 			scope, | ||||
| 			key, | ||||
| 		}); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| watch(() => props.path, fetchValue, { immediate: true }); | ||||
|  | ||||
| const headerActions = $computed(() => []); | ||||
|  | ||||
| const headerTabs = $computed(() => []); | ||||
|  | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.registry, | ||||
| 	icon: 'fas fa-cogs', | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
							
								
								
									
										74
									
								
								packages/client/src/pages/registry.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								packages/client/src/pages/registry.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| <template> | ||||
| <MkStickyContainer> | ||||
| 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> | ||||
| 	<MkSpacer :content-max="600"> | ||||
| 		<MkButton primary @click="createKey">{{ i18n.ts._registry.createKey }}</MkButton> | ||||
|  | ||||
| 		<FormSection v-if="scopes"> | ||||
| 			<template #label>{{ i18n.ts.system }}</template> | ||||
| 			<div class="_formLinks"> | ||||
| 				<FormLink v-for="scope in scopes" :to="`/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink> | ||||
| 			</div> | ||||
| 		</FormSection> | ||||
| 	</MkSpacer> | ||||
| </MkStickyContainer> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts" setup> | ||||
| import { ref, watch } from 'vue'; | ||||
| import JSON5 from 'json5'; | ||||
| import * as os from '@/os'; | ||||
| import { i18n } from '@/i18n'; | ||||
| import { definePageMetadata } from '@/scripts/page-metadata'; | ||||
| import FormLink from '@/components/form/link.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
|  | ||||
| let scopes = $ref(null); | ||||
|  | ||||
| function fetchScopes() { | ||||
| 	os.api('i/registry/scopes').then(res => { | ||||
| 		scopes = res.slice().sort((a, b) => a.join('/').localeCompare(b.join('/'))); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| async function createKey() { | ||||
| 	const { canceled, result } = await os.form(i18n.ts._registry.createKey, { | ||||
| 		key: { | ||||
| 			type: 'string', | ||||
| 			label: i18n.ts._registry.key, | ||||
| 		}, | ||||
| 		value: { | ||||
| 			type: 'string', | ||||
| 			multiline: true, | ||||
| 			label: i18n.ts.value, | ||||
| 		}, | ||||
| 		scope: { | ||||
| 			type: 'string', | ||||
| 			label: i18n.ts._registry.scope, | ||||
| 		}, | ||||
| 	}); | ||||
| 	if (canceled) return; | ||||
| 	os.apiWithDialog('i/registry/set', { | ||||
| 		scope: result.scope.split('/'), | ||||
| 		key: result.key, | ||||
| 		value: JSON5.parse(result.value), | ||||
| 	}).then(() => { | ||||
| 		fetchScopes(); | ||||
| 	}); | ||||
| } | ||||
|  | ||||
| fetchScopes(); | ||||
|  | ||||
| const headerActions = $computed(() => []); | ||||
|  | ||||
| const headerTabs = $computed(() => []); | ||||
|  | ||||
| definePageMetadata({ | ||||
| 	title: i18n.ts.registry, | ||||
| 	icon: 'fas fa-cogs', | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| </style> | ||||
| @@ -10,6 +10,8 @@ | ||||
|  | ||||
| 	<FormLink to="/settings/account-info" class="_formBlock">{{ i18n.ts.accountInfo }}</FormLink> | ||||
|  | ||||
| 	<FormLink to="/registry" class="_formBlock"><template #icon><i class="fas fa-cogs"></i></template>{{ i18n.ts.registry }}</FormLink> | ||||
|  | ||||
| 	<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink> | ||||
| </div> | ||||
| </template> | ||||
|   | ||||
| @@ -153,6 +153,15 @@ export const routes = [{ | ||||
| }, { | ||||
| 	path: '/channels', | ||||
| 	component: page(() => import('./pages/channels.vue')), | ||||
| }, { | ||||
| 	path: '/registry/keys/system/:path(*)?', | ||||
| 	component: page(() => import('./pages/registry.keys.vue')), | ||||
| }, { | ||||
| 	path: '/registry/value/system/:path(*)?', | ||||
| 	component: page(() => import('./pages/registry.value.vue')), | ||||
| }, { | ||||
| 	path: '/registry', | ||||
| 	component: page(() => import('./pages/registry.vue')), | ||||
| }, { | ||||
| 	path: '/admin/file/:fileId', | ||||
| 	component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo