diff options
| author | syuilo <syuilotan@yahoo.co.jp> | 2020-11-15 12:04:54 +0900 |
|---|---|---|
| committer | syuilo <syuilotan@yahoo.co.jp> | 2020-11-15 12:04:54 +0900 |
| commit | d53c55ecb512be114b394955da22e2450d01e379 (patch) | |
| tree | 10fd5da883c1617233670a771c453d9561dd2401 /src | |
| parent | Add description (diff) | |
| download | misskey-d53c55ecb512be114b394955da22e2450d01e379.tar.gz misskey-d53c55ecb512be114b394955da22e2450d01e379.tar.bz2 misskey-d53c55ecb512be114b394955da22e2450d01e379.zip | |
wip: clip
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/components/form-dialog.vue | 10 | ||||
| -rw-r--r-- | src/client/components/note.vue | 44 | ||||
| -rw-r--r-- | src/client/pages/my-antennas/index.vue | 2 | ||||
| -rw-r--r-- | src/client/pages/my-clips/index.vue | 78 | ||||
| -rw-r--r-- | src/client/pages/test.vue | 8 | ||||
| -rw-r--r-- | src/client/router.ts | 1 | ||||
| -rw-r--r-- | src/client/sidebar.ts | 8 | ||||
| -rw-r--r-- | src/models/entities/clip.ts | 6 | ||||
| -rw-r--r-- | src/models/repositories/clip.ts | 6 | ||||
| -rw-r--r-- | src/server/api/endpoints/clips/add-note.ts | 76 | ||||
| -rw-r--r-- | src/server/api/endpoints/clips/create.ts | 10 | ||||
| -rw-r--r-- | src/server/api/endpoints/clips/notes.ts | 9 |
12 files changed, 247 insertions, 11 deletions
diff --git a/src/client/components/form-dialog.vue b/src/client/components/form-dialog.vue index 2a067b67fa..0dc02258af 100644 --- a/src/client/components/form-dialog.vue +++ b/src/client/components/form-dialog.vue @@ -15,15 +15,15 @@ <div class="xkpnjxcv _section"> <label v-for="item in Object.keys(form).filter(item => !form[item].hidden)" :key="item"> <MkInput v-if="form[item].type === 'number'" v-model:value="values[item]" type="number" :step="form[item].step || 1"> - <span v-text="form[item].label || item"></span> + <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span> <template v-if="form[item].description" #desc>{{ form[item].description }}</template> </MkInput> - <MkInput v-else-if="form[item].type === 'string' && !item.multiline" v-model:value="values[item]" type="text"> - <span v-text="form[item].label || item"></span> + <MkInput v-else-if="form[item].type === 'string' && !form[item].multiline" v-model:value="values[item]" type="text"> + <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span> <template v-if="form[item].description" #desc>{{ form[item].description }}</template> </MkInput> - <MkTextarea v-else-if="form[item].type === 'string' && item.multiline" v-model:value="values[item]"> - <span v-text="form[item].label || item"></span> + <MkTextarea v-else-if="form[item].type === 'string' && form[item].multiline" v-model:value="values[item]"> + <span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $t('optional') }})</span> <template v-if="form[item].description" #desc>{{ form[item].description }}</template> </MkTextarea> <MkSwitch v-else-if="form[item].type === 'boolean'" v-model:value="values[item]"> diff --git a/src/client/components/note.vue b/src/client/components/note.vue index bf89cbf568..8aa205bdec 100644 --- a/src/client/components/note.vue +++ b/src/client/components/note.vue @@ -102,7 +102,7 @@ <script lang="ts"> import { computed, defineAsyncComponent, defineComponent, markRaw, ref } from 'vue'; -import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; +import { faSatelliteDish, faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug, faExclamationCircle, faPaperclip } from '@fortawesome/free-solid-svg-icons'; import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons'; import { parse } from '../../mfm/parse'; import { sum, unique } from '../../prelude/array'; @@ -610,6 +610,11 @@ export default defineComponent({ text: this.$t('favorite'), action: () => this.toggleFavorite(true) }), + { + icon: faPaperclip, + text: this.$t('clip'), + action: () => this.clip() + }, (this.appearNote.userId != this.$store.state.i.id) ? statePromise.then(state => state.isWatching ? { icon: faEyeSlash, text: this.$t('unwatch'), @@ -762,6 +767,43 @@ export default defineComponent({ }); }, + async clip() { + const clips = await os.api('clips/list'); + os.modalMenu([{ + icon: faPlus, + text: this.$t('createNew'), + action: async () => { + const { canceled, result } = await os.form(this.$t('createNewClip'), { + name: { + type: 'string', + label: this.$t('name') + }, + description: { + type: 'string', + required: false, + multiline: true, + label: this.$t('description') + }, + isPublic: { + type: 'boolean', + label: this.$t('public') + } + }); + if (canceled) return; + + const clip = await os.apiWithDialog('clips/create', result); + + os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); + } + }, null, ...clips.map(clip => ({ + text: clip.name, + action: () => { + os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: this.appearNote.id }); + } + }))], this.$refs.menuButton, { + }).then(this.focus); + }, + async promote() { const { canceled, result: days } = await os.dialog({ title: this.$t('numberOfDays'), diff --git a/src/client/pages/my-antennas/index.vue b/src/client/pages/my-antennas/index.vue index c4f8ce31f7..20b1024c9c 100644 --- a/src/client/pages/my-antennas/index.vue +++ b/src/client/pages/my-antennas/index.vue @@ -6,7 +6,7 @@ <XAntenna v-if="draft" :antenna="draft" @created="onAntennaCreated" style="margin-bottom: var(--margin);"/> <MkPagination :pagination="pagination" #default="{items}" class="antennas" ref="list"> - <XAntenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @created="onAntennaDeleted"/> + <XAntenna v-for="(antenna, i) in items" :key="antenna.id" :antenna="antenna" @deleted="onAntennaDeleted"/> </MkPagination> </div> </div> diff --git a/src/client/pages/my-clips/index.vue b/src/client/pages/my-clips/index.vue new file mode 100644 index 0000000000..93adb94a4b --- /dev/null +++ b/src/client/pages/my-clips/index.vue @@ -0,0 +1,78 @@ +<template> +<div class="_section"> + <MkButton @click="create" primary class="add"><Fa :icon="faPlus"/> {{ $t('add') }}</MkButton> + + <div class="_content"> + <MkPagination :pagination="pagination" #default="{items}" ref="list"> + <MkA v-for="item in items" :key="item.id" :to="`/clips/${item.id}`">{{ item.name }}</MkA> + </MkPagination> + </div> +</div> +</template> + +<script lang="ts"> +import { defineComponent } from 'vue'; +import { faPlus, faPaperclip } from '@fortawesome/free-solid-svg-icons'; +import MkPagination from '@/components/ui/pagination.vue'; +import MkButton from '@/components/ui/button.vue'; +import * as os from '@/os'; + +export default defineComponent({ + components: { + MkPagination, + MkButton, + }, + + data() { + return { + INFO: { + title: this.$t('clip'), + icon: faPaperclip, + action: { + icon: faPlus, + handler: this.create + } + }, + pagination: { + endpoint: 'clips/list', + limit: 10, + }, + draft: null, + faPlus + }; + }, + + methods: { + async create() { + const { canceled, result } = await os.form(this.$t('createNewClip'), { + name: { + type: 'string', + label: this.$t('name') + }, + description: { + type: 'string', + required: false, + multiline: true, + label: this.$t('description') + }, + isPublic: { + type: 'boolean', + label: this.$t('public') + } + }); + if (canceled) return; + + os.apiWithDialog('clips/create', result); + }, + + onClipCreated() { + this.$refs.list.reload(); + this.draft = null; + }, + + onClipDeleted() { + this.$refs.list.reload(); + }, + } +}); +</script> diff --git a/src/client/pages/test.vue b/src/client/pages/test.vue index 820cd950a1..77aa264c31 100644 --- a/src/client/pages/test.vue +++ b/src/client/pages/test.vue @@ -162,7 +162,7 @@ export default defineComponent({ dialogCancelByBgClick: true, dialogInput: false, dialogResult: null, - formTitle: null, + formTitle: 'Test form', formForm: JSON.stringify({ foo: { type: 'boolean', @@ -179,6 +179,12 @@ export default defineComponent({ default: 'Misskey makes you happy.', label: 'This is a string property' }, + qux: { + type: 'string', + multiline: true, + default: 'Misskey makes\nyou happy.', + label: 'Multiline string' + }, }, null, '\t'), formResult: null, mfm: '', diff --git a/src/client/router.ts b/src/client/router.ts index e8b6cfffd9..da2945be2c 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -55,6 +55,7 @@ export const router = createRouter({ { path: '/my/groups', component: page('my-groups/index') }, { path: '/my/groups/:group', component: page('my-groups/group') }, { path: '/my/antennas', component: page('my-antennas/index') }, + { path: '/my/clips', component: page('my-clips/index') }, { path: '/my/apps', component: page('apps') }, { path: '/scratchpad', component: page('scratchpad') }, { path: '/instance', component: page('instance/index') }, diff --git a/src/client/sidebar.ts b/src/client/sidebar.ts index 70f0bb3178..a541670df1 100644 --- a/src/client/sidebar.ts +++ b/src/client/sidebar.ts @@ -1,5 +1,5 @@ import { faBell, faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons'; -import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons'; +import { faAt, faBroadcastTower, faCloud, faColumns, faDoorClosed, faFileAlt, faFireAlt, faGamepad, faHashtag, faListUl, faPaperclip, faSatellite, faSatelliteDish, faSearch, faStar, faTerminal, faUserClock, faUsers } from '@fortawesome/free-solid-svg-icons'; import { computed } from 'vue'; import { store } from '@/store'; import { search } from '@/scripts/search'; @@ -99,6 +99,12 @@ export const sidebarDef = { show: computed(() => store.getters.isSignedIn), to: '/my/pages', }, + clips: { + title: 'clip', + icon: faPaperclip, + show: computed(() => store.getters.isSignedIn), + to: '/my/clips', + }, channels: { title: 'channel', icon: faSatelliteDish, diff --git a/src/models/entities/clip.ts b/src/models/entities/clip.ts index 37d21f73b1..66b5b8847e 100644 --- a/src/models/entities/clip.ts +++ b/src/models/entities/clip.ts @@ -35,4 +35,10 @@ export class Clip { default: false }) public isPublic: boolean; + + @Column('varchar', { + length: 2048, nullable: true, default: null, + comment: 'The description of the Clip.' + }) + public description: string | null; } diff --git a/src/models/repositories/clip.ts b/src/models/repositories/clip.ts index 9644ceec7e..7cc3fb7110 100644 --- a/src/models/repositories/clip.ts +++ b/src/models/repositories/clip.ts @@ -16,6 +16,7 @@ export class ClipRepository extends Repository<Clip> { id: clip.id, createdAt: clip.createdAt.toISOString(), name: clip.name, + description: clip.description, }; } } @@ -42,5 +43,10 @@ export const packedClipSchema = { optional: false as const, nullable: false as const, description: 'The name of the Clip.' }, + description: { + type: 'string' as const, + optional: false as const, nullable: true as const, + description: 'The description of the Clip.' + }, }, }; diff --git a/src/server/api/endpoints/clips/add-note.ts b/src/server/api/endpoints/clips/add-note.ts new file mode 100644 index 0000000000..4f5cc649e3 --- /dev/null +++ b/src/server/api/endpoints/clips/add-note.ts @@ -0,0 +1,76 @@ +import $ from 'cafy'; +import { ID } from '../../../../misc/cafy-id'; +import define from '../../define'; +import { ClipNotes, Clips } from '../../../../models'; +import { ApiError } from '../../error'; +import { genId } from '../../../../misc/gen-id'; +import { getNote } from '../../common/getters'; + +export const meta = { + tags: ['account', 'notes', 'clips'], + + requireCredential: true as const, + + kind: 'write:account', + + params: { + clipId: { + validator: $.type(ID), + }, + + noteId: { + validator: $.type(ID), + }, + }, + + errors: { + noSuchClip: { + message: 'No such clip.', + code: 'NO_SUCH_CLIP', + id: 'd6e76cc0-a1b5-4c7c-a287-73fa9c716dcf' + }, + + noSuchNote: { + message: 'No such note.', + code: 'NO_SUCH_NOTE', + id: 'fc8c0b49-c7a3-4664-a0a6-b418d386bb8b' + }, + + alreadyClipped: { + message: 'The note has already been clipped.', + code: 'ALREADY_CLIPPED', + id: '734806c4-542c-463a-9311-15c512803965' + }, + } +}; + +export default define(meta, async (ps, user) => { + const clip = await Clips.findOne({ + id: ps.clipId, + userId: user.id + }); + + if (clip == null) { + throw new ApiError(meta.errors.noSuchClip); + } + + const note = await getNote(ps.noteId).catch(e => { + if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote); + throw e; + }); + + const exist = await ClipNotes.findOne({ + noteId: note.id, + clipId: clip.id + }); + + if (exist != null) { + throw new ApiError(meta.errors.alreadyClipped); + } + + await ClipNotes.save({ + id: genId(), + noteId: note.id, + clipId: clip.id + }); +}); diff --git a/src/server/api/endpoints/clips/create.ts b/src/server/api/endpoints/clips/create.ts index f1b20c1157..0d122dbb9b 100644 --- a/src/server/api/endpoints/clips/create.ts +++ b/src/server/api/endpoints/clips/create.ts @@ -13,6 +13,14 @@ export const meta = { params: { name: { validator: $.str.range(1, 100) + }, + + isPublic: { + validator: $.optional.bool + }, + + description: { + validator: $.optional.nullable.str.range(1, 2048) } }, }; @@ -23,6 +31,8 @@ export default define(meta, async (ps, user) => { createdAt: new Date(), userId: user.id, name: ps.name, + isPublic: ps.isPublic, + description: ps.description, }); return await Clips.pack(clip); diff --git a/src/server/api/endpoints/clips/notes.ts b/src/server/api/endpoints/clips/notes.ts index 4cd7e8c621..5289533a1e 100644 --- a/src/server/api/endpoints/clips/notes.ts +++ b/src/server/api/endpoints/clips/notes.ts @@ -1,10 +1,11 @@ import $ from 'cafy'; import { ID } from '../../../../misc/cafy-id'; import define from '../../define'; -import { Clips, Notes } from '../../../../models'; +import { ClipNotes, Clips, Notes } from '../../../../models'; import { makePaginationQuery } from '../../common/make-pagination-query'; import { generateVisibilityQuery } from '../../common/generate-visibility-query'; import { generateMutedUserQuery } from '../../common/generate-muted-user-query'; +import { ApiError } from '../../error'; export const meta = { tags: ['account', 'notes', 'clips'], @@ -14,6 +15,10 @@ export const meta = { kind: 'read:account', params: { + clipId: { + validator: $.type(ID), + }, + limit: { validator: $.optional.num.range(1, 100), default: 10 @@ -30,7 +35,7 @@ export const meta = { errors: { noSuchClip: { - message: 'No such list.', + message: 'No such clip.', code: 'NO_SUCH_CLIP', id: '1d7645e6-2b6d-4635-b0fe-fe22b0e72e00' } |