Add group update / transfer API

This commit is contained in:
Satsuki Yanagi 2019-05-22 05:06:52 +09:00
parent 958686fad4
commit 7394adb16f
8 changed files with 240 additions and 6 deletions

View file

@ -762,6 +762,9 @@ common/views/components/user-group-editor.vue:
users: "メンバー" users: "メンバー"
rename: "グループ名を変更" rename: "グループ名を変更"
delete: "グループを削除" delete: "グループを削除"
transfer: "グループを譲渡"
transfer-are-you-sure: "グループ「$1」を「@$2」さんに譲渡しますか"
transferred: "グループを譲渡しました"
remove-user: "このグループから削除" remove-user: "このグループから削除"
delete-are-you-sure: "グループ「$1」を削除しますか" delete-are-you-sure: "グループ「$1」を削除しますか"
deleted: "削除しました" deleted: "削除しました"

View file

@ -7,6 +7,7 @@
<ui-margin> <ui-margin>
<ui-button @click="rename"><fa :icon="faICursor"/> {{ $t('rename') }}</ui-button> <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="del"><fa :icon="faTrashAlt"/> {{ $t('delete') }}</ui-button>
<ui-button @click="transfer"><fa :icon="faCrown"/> {{ $t('transfer') }}</ui-button>
</ui-margin> </ui-margin>
</section> </section>
</ui-container> </ui-container>
@ -28,9 +29,10 @@
<div> <div>
<header> <header>
<b><mk-user-name :user="user"/></b> <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> <span class="username">@{{ user | acct }}</span>
</header> </header>
<div> <div v-if="group.owner !== user.id">
<a @click="remove(user)">{{ $t('remove-user') }}</a> <a @click="remove(user)">{{ $t('remove-user') }}</a>
</div> </div>
</div> </div>
@ -44,7 +46,7 @@
<script lang="ts"> <script lang="ts">
import Vue from 'vue'; import Vue from 'vue';
import i18n from '../../../i18n'; 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'; import { faTrashAlt } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({ export default Vue.extend({
@ -60,7 +62,7 @@ export default Vue.extend({
return { return {
group: null, group: null,
users: [], users: [],
faICursor, faTrashAlt, faUsers, faPlus faCrown, faICursor, faTrashAlt, faUsers, faPlus
}; };
}, },
@ -78,6 +80,14 @@ export default Vue.extend({
}, },
methods: { methods: {
fetchGroup() {
this.$root.api('users/groups/show', {
groupId: this.group.id
}).then(group => {
this.group = group;
})
},
fetchUsers() { fetchUsers() {
this.$root.api('users/show', { this.$root.api('users/show', {
userIds: this.group.userIds userIds: this.group.userIds
@ -97,8 +107,15 @@ export default Vue.extend({
this.$root.api('users/groups/update', { this.$root.api('users/groups/update', {
groupId: this.group.id, groupId: this.group.id,
name: name name: name
}).then(() => {
this.fetchGroup();
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
}); });
}); });
})
}, },
del() { del() {
@ -130,12 +147,17 @@ export default Vue.extend({
groupId: this.group.id, groupId: this.group.id,
userId: user.id userId: user.id
}).then(() => { }).then(() => {
this.fetchGroup();
this.fetchUsers(); this.fetchUsers();
}).catch(e => {
this.$root.dialog({
type: 'error',
text: e
});
}); });
}, },
async invite() { async invite() {
const t = this.$t('invited');
const { result: user } = await this.$root.dialog({ const { result: user } = await this.$root.dialog({
user: { user: {
local: true local: true
@ -148,7 +170,44 @@ export default Vue.extend({
}).then(() => { }).then(() => {
this.$root.dialog({ this.$root.dialog({
type: 'success', 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 > header
color var(--text) 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 > .username
margin-left 8px margin-left 8px
opacity 0.7 opacity 0.7

View file

@ -78,6 +78,7 @@ import {
faKey, faKey,
faBan, faBan,
faCogs, faCogs,
faCrown,
faUnlockAlt, faUnlockAlt,
faPuzzlePiece, faPuzzlePiece,
faMobileAlt, faMobileAlt,
@ -210,6 +211,7 @@ library.add(
faKey, faKey,
faBan, faBan,
faCogs, faCogs,
faCrown,
faUnlockAlt, faUnlockAlt,
faPuzzlePiece, faPuzzlePiece,
faMobileAlt, faMobileAlt,

View file

@ -235,5 +235,8 @@
pageBlockBorder: 'rgba(255, 255, 255, 0.1)', pageBlockBorder: 'rgba(255, 255, 255, 0.1)',
pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)', pageBlockBorderHover: 'rgba(255, 255, 255, 0.15)',
groupUserListOwnerFg: '#f15f71',
groupUserListOwnerBg: '#5d282e'
}, },
} }

View file

@ -235,5 +235,8 @@
pageBlockBorder: 'rgba(0, 0, 0, 0.1)', pageBlockBorder: 'rgba(0, 0, 0, 0.1)',
pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)', pageBlockBorderHover: 'rgba(0, 0, 0, 0.15)',
groupUserListOwnerFg: '#f15f71',
groupUserListOwnerBg: '#ffdfdf'
}, },
} }

View file

@ -21,6 +21,7 @@ export class UserGroupRepository extends Repository<UserGroup> {
id: userGroup.id, id: userGroup.id,
createdAt: userGroup.createdAt.toISOString(), createdAt: userGroup.createdAt.toISOString(),
name: userGroup.name, name: userGroup.name,
owner: userGroup.userId,
userIds: users.map(x => x.userId) userIds: users.map(x => x.userId)
}; };
} }
@ -48,6 +49,11 @@ export const packedUserGroupSchema = {
optional: bool.false, nullable: bool.false, optional: bool.false, nullable: bool.false,
description: 'The name of the UserGroup.' description: 'The name of the UserGroup.'
}, },
owner: {
type: types.string,
nullable: bool.false, optional: bool.false,
format: 'id',
},
userIds: { userIds: {
type: types.array, type: types.array,
nullable: bool.false, optional: bool.true, nullable: bool.false, optional: bool.true,

View file

@ -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);
});

View file

@ -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);
});