mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-26 04:00:18 +01:00
Add group update / transfer API
This commit is contained in:
parent
a973bd56fe
commit
1092818203
8 changed files with 240 additions and 6 deletions
|
@ -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: "削除しました"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
86
src/server/api/endpoints/users/groups/transfer.ts
Normal file
86
src/server/api/endpoints/users/groups/transfer.ts
Normal 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);
|
||||||
|
});
|
62
src/server/api/endpoints/users/groups/update.ts
Normal file
62
src/server/api/endpoints/users/groups/update.ts
Normal 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);
|
||||||
|
});
|
Loading…
Reference in a new issue