ギャラリー投稿の編集と削除
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> | 						<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> | ||||||
| 					<div class="other"> | 					<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="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> | 						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button> | ||||||
| 					</div> | 					</div> | ||||||
|  | @ -84,6 +85,11 @@ export default defineComponent({ | ||||||
| 					title: this.post.title, | 					title: this.post.title, | ||||||
| 					text: this.post.description, | 					text: this.post.description, | ||||||
| 				}, | 				}, | ||||||
|  | 				actions: [{ | ||||||
|  | 					icon: 'fas fa-pencil-alt', | ||||||
|  | 					text: this.$ts.edit, | ||||||
|  | 					handler: this.edit | ||||||
|  | 				}] | ||||||
| 			} : null), | 			} : null), | ||||||
| 			otherPostsPagination: { | 			otherPostsPagination: { | ||||||
| 				endpoint: 'users/gallery/posts', | 				endpoint: 'users/gallery/posts', | ||||||
|  | @ -154,6 +160,10 @@ export default defineComponent({ | ||||||
| 				this.post.likedCount--; | 				this.post.likedCount--; | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  | 
 | ||||||
|  | 		edit() { | ||||||
|  | 			this.$router.push(`/gallery/${this.post.id}/edit`); | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | @ -38,7 +38,8 @@ export const router = createRouter({ | ||||||
| 		{ path: '/pages/new', component: page('page-editor/page-editor') }, | 		{ 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: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) }, | ||||||
| 		{ path: '/gallery', component: page('gallery/index') }, | 		{ 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: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) }, | ||||||
| 		{ path: '/channels', component: page('channels') }, | 		{ path: '/channels', component: page('channels') }, | ||||||
| 		{ path: '/channels/new', component: page('channel-editor') }, | 		{ 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