From 10928182034f1db668de422cda6461bd31aaaa73 Mon Sep 17 00:00:00 2001 From: Satsuki Yanagi <17376330+u1-liquid@users.noreply.github.com> Date: Wed, 22 May 2019 05:06:52 +0900 Subject: [PATCH 1/6] Add group update / transfer API --- locales/ja-JP.yml | 3 + .../common/views/pages/user-group-editor.vue | 81 +++++++++++++++-- src/client/app/init.ts | 2 + src/client/themes/dark.json5 | 3 + src/client/themes/light.json5 | 3 + src/models/repositories/user-group.ts | 6 ++ .../api/endpoints/users/groups/transfer.ts | 86 +++++++++++++++++++ .../api/endpoints/users/groups/update.ts | 62 +++++++++++++ 8 files changed, 240 insertions(+), 6 deletions(-) create mode 100644 src/server/api/endpoints/users/groups/transfer.ts create mode 100644 src/server/api/endpoints/users/groups/update.ts diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 9047378849..c3eb0bb523 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -762,6 +762,9 @@ common/views/components/user-group-editor.vue: users: "メンバー" rename: "グループ名を変更" delete: "グループを削除" + transfer: "グループを譲渡" + transfer-are-you-sure: "グループ「$1」を「@$2」さんに譲渡しますか?" + transferred: "グループを譲渡しました" remove-user: "このグループから削除" delete-are-you-sure: "グループ「$1」を削除しますか?" deleted: "削除しました" diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue index ef79689ae8..a32148cd7f 100644 --- a/src/client/app/common/views/pages/user-group-editor.vue +++ b/src/client/app/common/views/pages/user-group-editor.vue @@ -7,6 +7,7 @@ <ui-margin> <ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> <ui-button @click="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button> + <ui-button @click="transfer"><fa :icon="faCrown"/> {{ $t('transfer') }}</ui-button> </ui-margin> </section> </ui-container> @@ -28,9 +29,10 @@ <div> <header> <b><mk-user-name :user="user"/></b> + <span class="is-owner" v-if="group.owner === user.id">owner</span> <span class="username">@{{ user | acct }}</span> </header> - <div> + <div v-if="group.owner !== user.id"> <a @click="remove(user)">{{ $t('remove-user') }}</a> </div> </div> @@ -44,7 +46,7 @@ <script lang="ts"> import Vue from 'vue'; import i18n from '../../../i18n'; -import { faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; +import { faCrown, faICursor, faUsers, faPlus } from '@fortawesome/free-solid-svg-icons'; import { faTrashAlt } from '@fortawesome/free-regular-svg-icons'; export default Vue.extend({ @@ -60,7 +62,7 @@ export default Vue.extend({ return { group: null, users: [], - faICursor, faTrashAlt, faUsers, faPlus + faCrown, faICursor, faTrashAlt, faUsers, faPlus }; }, @@ -78,6 +80,14 @@ export default Vue.extend({ }, methods: { + fetchGroup() { + this.$root.api('users/groups/show', { + groupId: this.group.id + }).then(group => { + this.group = group; + }) + }, + fetchUsers() { this.$root.api('users/show', { userIds: this.group.userIds @@ -97,8 +107,15 @@ export default Vue.extend({ this.$root.api('users/groups/update', { groupId: this.group.id, name: name + }).then(() => { + this.fetchGroup(); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); }); - }); + }) }, del() { @@ -130,12 +147,17 @@ export default Vue.extend({ groupId: this.group.id, userId: user.id }).then(() => { + this.fetchGroup(); this.fetchUsers(); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); }); }, async invite() { - const t = this.$t('invited'); const { result: user } = await this.$root.dialog({ user: { local: true @@ -148,7 +170,44 @@ export default Vue.extend({ }).then(() => { this.$root.dialog({ type: 'success', - text: t + text: this.$t('invited') + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); + }); + }, + + async transfer() { + const { result: user } = await this.$root.dialog({ + user: { + local: true + } + }); + if (user == null) return; + + this.$root.dialog({ + type: 'warning', + text: this.$t('transfer-are-you-sure').replace('$1', this.group.name).replace('$2', user.username), + showCancelButton: true + }).then(({ canceled }) => { + if (canceled) return; + + this.$root.api('users/groups/transfer', { + groupId: this.group.id, + userId: user.id + }).then(() => { + this.$root.dialog({ + type: 'success', + text: this.$t('transferred') + }); + }).catch(e => { + this.$root.dialog({ + type: 'error', + text: e + }); }); }); } @@ -179,6 +238,16 @@ export default Vue.extend({ > header color var(--text) + > .is-owner + flex-shrink 0 + align-self center + margin-left 8px + padding 1px 6px + font-size 80% + background var(--groupUserListOwnerBg) + color var(--groupUserListOwnerFg) + border-radius 3px + > .username margin-left 8px opacity 0.7 diff --git a/src/client/app/init.ts b/src/client/app/init.ts index da7baff4fe..52da380e84 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -78,6 +78,7 @@ import { faKey, faBan, faCogs, + faCrown, faUnlockAlt, faPuzzlePiece, faMobileAlt, @@ -210,6 +211,7 @@ library.add( faKey, faBan, faCogs, + faCrown, faUnlockAlt, faPuzzlePiece, faMobileAlt, diff --git a/src/client/themes/dark.json5 b/src/client/themes/dark.json5 index 8e0c726b4c..0665d59901 100644 --- a/src/client/themes/dark.json5 +++ b/src/client/themes/dark.json5 @@ -235,5 +235,8 @@ pageBlockBorder: 'rgba(255, 255, 255, 0.1)', pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)', + + groupUserListOwnerFg: '#f15f71', + groupUserListOwnerBg: '#5d282e' }, } diff --git a/src/client/themes/light.json5 b/src/client/themes/light.json5 index 1fff18176a..cbe456ca5d 100644 --- a/src/client/themes/light.json5 +++ b/src/client/themes/light.json5 @@ -235,5 +235,8 @@ pageBlockBorder: 'rgba(0, 0, 0, 0.1)', pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)', + + groupUserListOwnerFg: '#f15f71', + groupUserListOwnerBg: '#ffdfdf' }, } diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts index 8bb1ae8330..dbbe8bf84c 100644 --- a/src/models/repositories/user-group.ts +++ b/src/models/repositories/user-group.ts @@ -21,6 +21,7 @@ export class UserGroupRepository extends Repository<UserGroup> { id: userGroup.id, createdAt: userGroup.createdAt.toISOString(), name: userGroup.name, + owner: userGroup.userId, userIds: users.map(x => x.userId) }; } @@ -48,6 +49,11 @@ export const packedUserGroupSchema = { optional: bool.false, nullable: bool.false, description: 'The name of the UserGroup.' }, + owner: { + type: types.string, + nullable: bool.false, optional: bool.false, + format: 'id', + }, userIds: { type: types.array, nullable: bool.false, optional: bool.true, diff --git a/src/server/api/endpoints/users/groups/transfer.ts b/src/server/api/endpoints/users/groups/transfer.ts new file mode 100644 index 0000000000..3baa182abf --- /dev/null +++ b/src/server/api/endpoints/users/groups/transfer.ts @@ -0,0 +1,86 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { getUser } from '../../../common/getters'; +import { UserGroups, UserGroupJoinings } from '../../../../../models'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループを指定したユーザーグループ内のユーザーに譲渡します。', + 'en-US': 'Transfer user group ownership to another user in group.' + }, + + tags: ['groups', 'users'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + }, + + userId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象のユーザーのID', + 'en-US': 'Target user ID' + } + }, + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '8e31d36b-2f88-4ccd-a438-e2d78a9162db' + }, + + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: '711f7ebb-bbb9-4dfa-b540-b27809fed5e9' + }, + + noSuchGroupMember: { + message: 'No such group member.', + code: 'NO_SUCH_GROUP_MEMBER', + id: 'd31bebee-196d-42c2-9a3e-9474d4be6cc4' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id, + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + // Fetch the user + const user = await getUser(ps.userId).catch(e => { + if (e.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); + throw e; + }); + + const joining = await UserGroupJoinings.findOne({ + userGroupId: userGroup.id, + userId: user.id + }); + + if (!joining) { + throw new ApiError(meta.errors.noSuchGroupMember); + } + + await UserGroups.update(userGroup.id, { + userId: ps.userId + }); + + return await UserGroups.pack(userGroup.id); +}); diff --git a/src/server/api/endpoints/users/groups/update.ts b/src/server/api/endpoints/users/groups/update.ts new file mode 100644 index 0000000000..ad9a1faa23 --- /dev/null +++ b/src/server/api/endpoints/users/groups/update.ts @@ -0,0 +1,62 @@ +import $ from 'cafy'; +import { ID } from '../../../../../misc/cafy-id'; +import define from '../../../define'; +import { ApiError } from '../../../error'; +import { UserGroups } from '../../../../../models'; + +export const meta = { + desc: { + 'ja-JP': '指定したユーザーグループを更新します。', + 'en-US': 'Update a user group' + }, + + tags: ['groups'], + + requireCredential: true, + + kind: 'write:user-groups', + + params: { + groupId: { + validator: $.type(ID), + desc: { + 'ja-JP': '対象となるユーザーグループのID', + 'en-US': 'ID of target user group' + } + }, + + name: { + validator: $.str.range(1, 100), + desc: { + 'ja-JP': 'このユーザーグループの名前', + 'en-US': 'name of this user group' + } + } + }, + + errors: { + noSuchGroup: { + message: 'No such group.', + code: 'NO_SUCH_GROUP', + id: '9081cda3-7a9e-4fac-a6ce-908d70f282f6' + }, + } +}; + +export default define(meta, async (ps, me) => { + // Fetch the group + const userGroup = await UserGroups.findOne({ + id: ps.groupId, + userId: me.id + }); + + if (userGroup == null) { + throw new ApiError(meta.errors.noSuchGroup); + } + + await UserGroups.update(userGroup.id, { + name: ps.name + }); + + return await UserGroups.pack(userGroup.id); +}); From c7456224af3fbe39b3207f936781ac9b09a37a5f Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 22 May 2019 12:55:53 +0900 Subject: [PATCH 2/6] Fix bug --- src/client/app/common/views/pages/user-group-editor.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/app/common/views/pages/user-group-editor.vue b/src/client/app/common/views/pages/user-group-editor.vue index a32148cd7f..3cfecd837f 100644 --- a/src/client/app/common/views/pages/user-group-editor.vue +++ b/src/client/app/common/views/pages/user-group-editor.vue @@ -158,6 +158,7 @@ export default Vue.extend({ }, async invite() { + const t = this.$t('invited'); const { result: user } = await this.$root.dialog({ user: { local: true @@ -170,7 +171,7 @@ export default Vue.extend({ }).then(() => { this.$root.dialog({ type: 'success', - text: this.$t('invited') + text: t }); }).catch(e => { this.$root.dialog({ From 50d4de19f0d93c78f70f7821bd9798317bd6b8d6 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 22 May 2019 12:56:42 +0900 Subject: [PATCH 3/6] Clean up --- src/client/app/init.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/app/init.ts b/src/client/app/init.ts index 52da380e84..da7baff4fe 100644 --- a/src/client/app/init.ts +++ b/src/client/app/init.ts @@ -78,7 +78,6 @@ import { faKey, faBan, faCogs, - faCrown, faUnlockAlt, faPuzzlePiece, faMobileAlt, @@ -211,7 +210,6 @@ library.add( faKey, faBan, faCogs, - faCrown, faUnlockAlt, faPuzzlePiece, faMobileAlt, From 4a88cb596815c77630be7d01d40687c49b0560d2 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 22 May 2019 12:58:44 +0900 Subject: [PATCH 4/6] rename --- src/models/repositories/user-group.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/repositories/user-group.ts b/src/models/repositories/user-group.ts index dbbe8bf84c..38174ce235 100644 --- a/src/models/repositories/user-group.ts +++ b/src/models/repositories/user-group.ts @@ -21,7 +21,7 @@ export class UserGroupRepository extends Repository<UserGroup> { id: userGroup.id, createdAt: userGroup.createdAt.toISOString(), name: userGroup.name, - owner: userGroup.userId, + ownerId: userGroup.userId, userIds: users.map(x => x.userId) }; } @@ -49,7 +49,7 @@ export const packedUserGroupSchema = { optional: bool.false, nullable: bool.false, description: 'The name of the UserGroup.' }, - owner: { + ownerId: { type: types.string, nullable: bool.false, optional: bool.false, format: 'id', From eed9266ecbc26282dcc349eaacaa49d8d3a812e1 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 22 May 2019 12:58:53 +0900 Subject: [PATCH 5/6] align format --- src/server/api/endpoints/users/groups/transfer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/endpoints/users/groups/transfer.ts b/src/server/api/endpoints/users/groups/transfer.ts index 3baa182abf..ca0ab9ab29 100644 --- a/src/server/api/endpoints/users/groups/transfer.ts +++ b/src/server/api/endpoints/users/groups/transfer.ts @@ -74,7 +74,7 @@ export default define(meta, async (ps, me) => { userId: user.id }); - if (!joining) { + if (joining == null) { throw new ApiError(meta.errors.noSuchGroupMember); } From 9d75ec799b15758b6da69575ab8355735e036f28 Mon Sep 17 00:00:00 2001 From: syuilo <syuilotan@yahoo.co.jp> Date: Wed, 22 May 2019 13:00:36 +0900 Subject: [PATCH 6/6] Better deninition --- src/server/api/endpoints/users/groups/transfer.ts | 7 +++++++ src/server/api/endpoints/users/groups/update.ts | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/server/api/endpoints/users/groups/transfer.ts b/src/server/api/endpoints/users/groups/transfer.ts index ca0ab9ab29..b4284ab484 100644 --- a/src/server/api/endpoints/users/groups/transfer.ts +++ b/src/server/api/endpoints/users/groups/transfer.ts @@ -4,6 +4,7 @@ import define from '../../../define'; import { ApiError } from '../../../error'; import { getUser } from '../../../common/getters'; import { UserGroups, UserGroupJoinings } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; export const meta = { desc: { @@ -31,6 +32,12 @@ export const meta = { }, }, + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + }, + errors: { noSuchGroup: { message: 'No such group.', diff --git a/src/server/api/endpoints/users/groups/update.ts b/src/server/api/endpoints/users/groups/update.ts index ad9a1faa23..bc974621a3 100644 --- a/src/server/api/endpoints/users/groups/update.ts +++ b/src/server/api/endpoints/users/groups/update.ts @@ -3,6 +3,7 @@ import { ID } from '../../../../../misc/cafy-id'; import define from '../../../define'; import { ApiError } from '../../../error'; import { UserGroups } from '../../../../../models'; +import { types, bool } from '../../../../../misc/schema'; export const meta = { desc: { @@ -34,6 +35,12 @@ export const meta = { } }, + res: { + type: types.object, + optional: bool.false, nullable: bool.false, + ref: 'UserGroup', + }, + errors: { noSuchGroup: { message: 'No such group.',