feat(client): registry editor
This commit is contained in:
		
							parent
							
								
									a3f3ef4226
								
							
						
					
					
						commit
						003f592ef6
					
				
					 6 changed files with 312 additions and 0 deletions
				
			
		|  | @ -9,6 +9,14 @@ | ||||||
| You should also include the user name that made the change. | You should also include the user name that made the change. | ||||||
| --> | --> | ||||||
| 
 | 
 | ||||||
|  | ## 12.x.x (unreleased) | ||||||
|  | 
 | ||||||
|  | ### Improvements | ||||||
|  | - Client: registry editor @syuilo | ||||||
|  | 
 | ||||||
|  | ### Bugfixes | ||||||
|  | -  | ||||||
|  | 
 | ||||||
| ## 12.115.0 (2022/07/16) | ## 12.115.0 (2022/07/16) | ||||||
| 
 | 
 | ||||||
| ### Improvements | ### Improvements | ||||||
|  |  | ||||||
							
								
								
									
										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="/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> | 	<FormLink to="/settings/delete-account" class="_formBlock"><template #icon><i class="fas fa-exclamation-triangle"></i></template>{{ i18n.ts.closeAccount }}</FormLink> | ||||||
| </div> | </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|  | @ -153,6 +153,15 @@ export const routes = [{ | ||||||
| }, { | }, { | ||||||
| 	path: '/channels', | 	path: '/channels', | ||||||
| 	component: page(() => import('./pages/channels.vue')), | 	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', | 	path: '/admin/file/:fileId', | ||||||
| 	component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), | 	component: iAmModerator ? page(() => import('./pages/admin-file.vue')) : page(() => import('./pages/not-found.vue')), | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue