ギャラリー投稿の編集と削除
This commit is contained in:
		
							parent
							
								
									8bce241170
								
							
						
					
					
						commit
						42539575a6
					
				
					 6 changed files with 301 additions and 111 deletions
				
			
		
							
								
								
									
										168
									
								
								src/client/pages/gallery/edit.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/client/pages/gallery/edit.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,168 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormSuspense :p="init"> | ||||
| 		<FormInput v-model:value="title"> | ||||
| 			<span>{{ $ts.title }}</span> | ||||
| 		</FormInput> | ||||
| 
 | ||||
| 		<FormTextarea v-model:value="description" :max="500"> | ||||
| 			<span>{{ $ts.description }}</span> | ||||
| 		</FormTextarea> | ||||
| 
 | ||||
| 		<FormGroup> | ||||
| 			<div v-for="file in files" :key="file.id" class="_formItem _formPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> | ||||
| 				<div class="name">{{ file.name }}</div> | ||||
| 				<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button> | ||||
| 			</div> | ||||
| 			<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton> | ||||
| 		</FormGroup> | ||||
| 
 | ||||
| 		<FormSwitch v-model:value="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch> | ||||
| 
 | ||||
| 		<FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton> | ||||
| 		<FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton> | ||||
| 
 | ||||
| 		<FormButton v-if="postId" @click="del" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton> | ||||
| 	</FormSuspense> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { computed, defineComponent } from 'vue'; | ||||
| import FormButton from '@client/components/form/button.vue'; | ||||
| import FormInput from '@client/components/form/input.vue'; | ||||
| import FormTextarea from '@client/components/form/textarea.vue'; | ||||
| import FormSwitch from '@client/components/form/switch.vue'; | ||||
| import FormTuple from '@client/components/form/tuple.vue'; | ||||
| import FormBase from '@client/components/form/base.vue'; | ||||
| import FormGroup from '@client/components/form/group.vue'; | ||||
| import FormSuspense from '@client/components/form/suspense.vue'; | ||||
| import { selectFile } from '@client/scripts/select-file'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormButton, | ||||
| 		FormInput, | ||||
| 		FormTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormBase, | ||||
| 		FormGroup, | ||||
| 		FormSuspense, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		postId: { | ||||
| 			type: String, | ||||
| 			required: false, | ||||
| 			default: null, | ||||
| 		} | ||||
| 	}, | ||||
| 	 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.postId ? { | ||||
| 				title: this.$ts.edit, | ||||
| 				icon: 'fas fa-pencil-alt' | ||||
| 			} : { | ||||
| 				title: this.$ts.postToGallery, | ||||
| 				icon: 'fas fa-pencil-alt' | ||||
| 			}), | ||||
| 			init: null, | ||||
| 			files: [], | ||||
| 			description: null, | ||||
| 			title: null, | ||||
| 			isSensitive: false, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		postId: { | ||||
| 			handler() { | ||||
| 				this.init = () => this.postId ? os.api('gallery/posts/show', { | ||||
| 					postId: this.postId | ||||
| 				}).then(post => { | ||||
| 					this.files = post.files; | ||||
| 					this.title = post.title; | ||||
| 					this.description = post.description; | ||||
| 					this.isSensitive = post.isSensitive; | ||||
| 				}) : Promise.resolve(null); | ||||
| 			}, | ||||
| 			immediate: true, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		selectFile(e) { | ||||
| 			selectFile(e.currentTarget || e.target, null, true).then(files => { | ||||
| 				this.files = this.files.concat(files); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		remove(file) { | ||||
| 			this.files = this.files.filter(f => f.id !== file.id); | ||||
| 		}, | ||||
| 
 | ||||
| 		async save() { | ||||
| 			if (this.postId) { | ||||
| 				await os.apiWithDialog('gallery/posts/update', { | ||||
| 					postId: this.postId, | ||||
| 					title: this.title, | ||||
| 					description: this.description, | ||||
| 					fileIds: this.files.map(file => file.id), | ||||
| 					isSensitive: this.isSensitive, | ||||
| 				}); | ||||
| 				this.$router.push(`/gallery/${this.postId}`); | ||||
| 			} else { | ||||
| 				const post = await os.apiWithDialog('gallery/posts/create', { | ||||
| 					title: this.title, | ||||
| 					description: this.description, | ||||
| 					fileIds: this.files.map(file => file.id), | ||||
| 					isSensitive: this.isSensitive, | ||||
| 				}); | ||||
| 				this.$router.push(`/gallery/${post.id}`); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		async del() { | ||||
| 			const { canceled } = await os.dialog({ | ||||
| 				type: 'warning', | ||||
| 				text: this.$ts.deleteConfirm, | ||||
| 				showCancelButton: true | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 			await os.apiWithDialog('gallery/posts/delete', { | ||||
| 				postId: this.postId, | ||||
| 			}); | ||||
| 			this.$router.push(`/gallery`); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .wqugxsfx { | ||||
| 	height: 200px; | ||||
| 	background-size: contain; | ||||
| 	background-position: center; | ||||
| 	background-repeat: no-repeat; | ||||
| 	position: relative; | ||||
| 
 | ||||
| 	> .name { | ||||
| 		position: absolute; | ||||
| 		top: 8px; | ||||
| 		left: 9px; | ||||
| 		padding: 8px; | ||||
| 		background: var(--panel); | ||||
| 	} | ||||
| 
 | ||||
| 	> .remove { | ||||
| 		position: absolute; | ||||
| 		top: 8px; | ||||
| 		right: 9px; | ||||
| 		padding: 8px; | ||||
| 		background: var(--panel); | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -1,110 +0,0 @@ | |||
| <template> | ||||
| <FormBase> | ||||
| 	<FormInput v-model:value="title"> | ||||
| 		<span>{{ $ts.title }}</span> | ||||
| 	</FormInput> | ||||
| 
 | ||||
| 	<FormTextarea v-model:value="description" :max="500"> | ||||
| 		<span>{{ $ts.description }}</span> | ||||
| 	</FormTextarea> | ||||
| 
 | ||||
| 	<FormGroup> | ||||
| 		<div v-for="file in files" :key="file.id" class="_formItem _formPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }"> | ||||
| 			<div class="name">{{ file.name }}</div> | ||||
| 			<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button> | ||||
| 		</div> | ||||
| 		<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton> | ||||
| 	</FormGroup> | ||||
| 
 | ||||
| 	<FormSwitch v-model:value="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch> | ||||
| 
 | ||||
| 	<FormButton @click="publish" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton> | ||||
| </FormBase> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import FormButton from '@client/components/form/button.vue'; | ||||
| import FormInput from '@client/components/form/input.vue'; | ||||
| import FormTextarea from '@client/components/form/textarea.vue'; | ||||
| import FormSwitch from '@client/components/form/switch.vue'; | ||||
| import FormTuple from '@client/components/form/tuple.vue'; | ||||
| import FormBase from '@client/components/form/base.vue'; | ||||
| import FormGroup from '@client/components/form/group.vue'; | ||||
| import { selectFile } from '@client/scripts/select-file'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormButton, | ||||
| 		FormInput, | ||||
| 		FormTextarea, | ||||
| 		FormSwitch, | ||||
| 		FormBase, | ||||
| 		FormGroup, | ||||
| 	}, | ||||
| 	 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.postToGallery, | ||||
| 				icon: 'fas fa-pencil-alt' | ||||
| 			}, | ||||
| 			files: [], | ||||
| 			description: null, | ||||
| 			title: null, | ||||
| 			isSensitive: false, | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		selectFile(e) { | ||||
| 			selectFile(e.currentTarget || e.target, null, true).then(files => { | ||||
| 				this.files = this.files.concat(files); | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		remove(file) { | ||||
| 			this.files = this.files.filter(f => f.id !== file.id); | ||||
| 		}, | ||||
| 
 | ||||
| 		async publish() { | ||||
| 			const post = await os.apiWithDialog('gallery/posts/create', { | ||||
| 				title: this.title, | ||||
| 				description: this.description, | ||||
| 				fileIds: this.files.map(file => file.id), | ||||
| 				isSensitive: this.isSensitive, | ||||
| 			}); | ||||
| 
 | ||||
| 			this.$router.push(`/gallery/${post.id}`); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .wqugxsfx { | ||||
| 	height: 200px; | ||||
| 	background-size: contain; | ||||
| 	background-position: center; | ||||
| 	background-repeat: no-repeat; | ||||
| 	position: relative; | ||||
| 
 | ||||
| 	> .name { | ||||
| 		position: absolute; | ||||
| 		top: 8px; | ||||
| 		left: 9px; | ||||
| 		padding: 8px; | ||||
| 		background: var(--panel); | ||||
| 	} | ||||
| 
 | ||||
| 	> .remove { | ||||
| 		position: absolute; | ||||
| 		top: 8px; | ||||
| 		right: 9px; | ||||
| 		padding: 8px; | ||||
| 		background: var(--panel); | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -19,6 +19,7 @@ | |||
| 						<MkButton class="button" @click="like()" v-else v-tooltip="$ts._gallery.like"><i class="far fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton> | ||||
| 					</div> | ||||
| 					<div class="other"> | ||||
| 						<button v-if="$i && $i.id === post.user.id" class="_button" @click="edit" v-tooltip="$ts.edit" v-click-anime><i class="fas fa-pencil-alt fa-fw"></i></button> | ||||
| 						<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button> | ||||
| 						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button> | ||||
| 					</div> | ||||
|  | @ -84,6 +85,11 @@ export default defineComponent({ | |||
| 					title: this.post.title, | ||||
| 					text: this.post.description, | ||||
| 				}, | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-pencil-alt', | ||||
| 					text: this.$ts.edit, | ||||
| 					handler: this.edit | ||||
| 				}] | ||||
| 			} : null), | ||||
| 			otherPostsPagination: { | ||||
| 				endpoint: 'users/gallery/posts', | ||||
|  | @ -154,6 +160,10 @@ export default defineComponent({ | |||
| 				this.post.likedCount--; | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
| 		edit() { | ||||
| 			this.$router.push(`/gallery/${this.post.id}/edit`); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  |  | |||
|  | @ -38,7 +38,8 @@ export const router = createRouter({ | |||
| 		{ path: '/pages/new', component: page('page-editor/page-editor') }, | ||||
| 		{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, | ||||
| 		{ path: '/gallery', component: page('gallery/index') }, | ||||
| 		{ path: '/gallery/new', component: page('gallery/new') }, | ||||
| 		{ path: '/gallery/new', component: page('gallery/edit') }, | ||||
| 		{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) }, | ||||
| 		{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) }, | ||||
| 		{ path: '/channels', component: page('channels') }, | ||||
| 		{ path: '/channels/new', component: page('channel-editor') }, | ||||
|  |  | |||
							
								
								
									
										40
									
								
								src/server/api/endpoints/gallery/posts/delete.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/server/api/endpoints/gallery/posts/delete.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,40 @@ | |||
| import $ from 'cafy'; | ||||
| import define from '../../../define'; | ||||
| import { ApiError } from '../../../error'; | ||||
| import { GalleryPosts } from '../../../../../models'; | ||||
| import { ID } from '@/misc/cafy-id'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['gallery'], | ||||
| 
 | ||||
| 	requireCredential: true as const, | ||||
| 
 | ||||
| 	kind: 'write:gallery', | ||||
| 
 | ||||
| 	params: { | ||||
| 		postId: { | ||||
| 			validator: $.type(ID), | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	errors: { | ||||
| 		noSuchPost: { | ||||
| 			message: 'No such post.', | ||||
| 			code: 'NO_SUCH_POST', | ||||
| 			id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5' | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps, user) => { | ||||
| 	const post = await GalleryPosts.findOne({ | ||||
| 		id: ps.postId, | ||||
| 		userId: user.id, | ||||
| 	}); | ||||
| 
 | ||||
| 	if (post == null) { | ||||
| 		throw new ApiError(meta.errors.noSuchPost); | ||||
| 	} | ||||
| 
 | ||||
| 	await GalleryPosts.delete(post.id); | ||||
| }); | ||||
							
								
								
									
										81
									
								
								src/server/api/endpoints/gallery/posts/update.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/server/api/endpoints/gallery/posts/update.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,81 @@ | |||
| import $ from 'cafy'; | ||||
| import * as ms from 'ms'; | ||||
| import define from '../../../define'; | ||||
| import { ID } from '../../../../../misc/cafy-id'; | ||||
| import { DriveFiles, GalleryPosts } from '../../../../../models'; | ||||
| import { GalleryPost } from '../../../../../models/entities/gallery-post'; | ||||
| import { ApiError } from '../../../error'; | ||||
| 
 | ||||
| export const meta = { | ||||
| 	tags: ['gallery'], | ||||
| 
 | ||||
| 	requireCredential: true as const, | ||||
| 
 | ||||
| 	kind: 'write:gallery', | ||||
| 
 | ||||
| 	limit: { | ||||
| 		duration: ms('1hour'), | ||||
| 		max: 300 | ||||
| 	}, | ||||
| 
 | ||||
| 	params: { | ||||
| 		postId: { | ||||
| 			validator: $.type(ID), | ||||
| 		}, | ||||
| 
 | ||||
| 		title: { | ||||
| 			validator: $.str.min(1), | ||||
| 		}, | ||||
| 
 | ||||
| 		description: { | ||||
| 			validator: $.optional.nullable.str, | ||||
| 		}, | ||||
| 
 | ||||
| 		fileIds: { | ||||
| 			validator: $.arr($.type(ID)).unique().range(1, 32), | ||||
| 		}, | ||||
| 
 | ||||
| 		isSensitive: { | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	res: { | ||||
| 		type: 'object' as const, | ||||
| 		optional: false as const, nullable: false as const, | ||||
| 		ref: 'GalleryPost', | ||||
| 	}, | ||||
| 
 | ||||
| 	errors: { | ||||
| 
 | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps, user) => { | ||||
| 	const files = (await Promise.all(ps.fileIds.map(fileId => | ||||
| 		DriveFiles.findOne({ | ||||
| 			id: fileId, | ||||
| 			userId: user.id | ||||
| 		}) | ||||
| 	))).filter(file => file != null); | ||||
| 
 | ||||
| 	if (files.length === 0) { | ||||
| 		throw new Error(); | ||||
| 	} | ||||
| 
 | ||||
| 	await GalleryPosts.update({ | ||||
| 		id: ps.postId, | ||||
| 		userId: user.id, | ||||
| 	}, { | ||||
| 		updatedAt: new Date(), | ||||
| 		title: ps.title, | ||||
| 		description: ps.description, | ||||
| 		isSensitive: ps.isSensitive, | ||||
| 		fileIds: files.map(file => file.id) | ||||
| 	}); | ||||
| 
 | ||||
| 	const post = await GalleryPosts.findOneOrFail(ps.postId); | ||||
| 
 | ||||
| 	return await GalleryPosts.pack(post, user); | ||||
| }); | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue