From 38dbb84a959fb92cd58d97948bea18081540eec2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:07:51 +0000 Subject: [PATCH 1/2] Initial plan From c80983ae1b2618d9259c5dc4890141ef98841f7f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:33:05 +0000 Subject: [PATCH 2/2] fix: channel settings bugs - permissions, co-admin list, JSON save error - Frontend: Add isOwner variable; show transfer button only to channel owner (not all root/moderator users) - Frontend: Fix save() to not send collaboratorIds when user lacks permission (co-admins always sent empty collaboratorIds causing access denied errors) - Frontend: Add missing CSS styles for collaborator user list items - Backend: Move collaboratorIds update into main channelsRepository.update() call to fix JSON serialization error (was in separate setCollaborators call) - Backend: Handle empty collaboratorIds array correctly (skip In([]) check) - Backend: Fix entity column definition to use explicit varchar type with correct length 64 matching the migration Agent-Logs-Url: https://github.com/Type4ny-Project/Misskey/sessions/b0bc8330-a8a3-4641-bf50-6cbd2ff258fb Co-authored-by: mattyatea <56515516+mattyatea@users.noreply.github.com> --- packages/backend/src/models/Channel.ts | 5 +- .../server/api/endpoints/channels/update.ts | 22 ++++--- .../frontend/src/pages/channel-editor.vue | 59 ++++++++++++++----- pnpm-lock.yaml | 38 +++++++++++- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/packages/backend/src/models/Channel.ts b/packages/backend/src/models/Channel.ts index 020c3912bb7..b07a3b00600 100644 --- a/packages/backend/src/models/Channel.ts +++ b/packages/backend/src/models/Channel.ts @@ -105,9 +105,8 @@ export class MiChannel { }) public isLocalOnly: boolean; - @Column({ - ...id(), - array: true, default: '{}', + @Column('varchar', { + array: true, length: 64, default: '{}', comment: 'Collaborator user IDs.', }) public collaboratorIds: MiUser['id'][]; diff --git a/packages/backend/src/server/api/endpoints/channels/update.ts b/packages/backend/src/server/api/endpoints/channels/update.ts index 9c72e0fdbd6..05cdceb3f69 100644 --- a/packages/backend/src/server/api/endpoints/channels/update.ts +++ b/packages/backend/src/server/api/endpoints/channels/update.ts @@ -120,21 +120,24 @@ export default class extends Endpoint { // eslint- banner = null; } + let validatedCollaboratorIds: string[] | undefined = undefined; if (ps.collaboratorIds !== undefined) { if (channel.userId !== me.id && !iAmModerator) { throw new ApiError(meta.errors.accessDenied); } - const users = await this.usersRepository.findBy({ - id: In(ps.collaboratorIds), - }); - if (users.length !== ps.collaboratorIds.length) { - throw new ApiError({ - message: 'One or more collaborator user IDs are invalid.', - code: 'INVALID_COLLABORATOR_USER_IDS', - id: '3e7c9a2b-4f8c-4d1e-9b7a-3f6e8c7d9a1b', + if (ps.collaboratorIds.length > 0) { + const users = await this.usersRepository.findBy({ + id: In(ps.collaboratorIds), }); + if (users.length !== ps.collaboratorIds.length) { + throw new ApiError({ + message: 'One or more collaborator user IDs are invalid.', + code: 'INVALID_COLLABORATOR_USER_IDS', + id: '3e7c9a2b-4f8c-4d1e-9b7a-3f6e8c7d9a1b', + }); + } } - await this.channelService.setCollaborators(channel, ps.collaboratorIds); + validatedCollaboratorIds = ps.collaboratorIds; } if (ps.isLocalOnly !== undefined) channel.isLocalOnly = ps.isLocalOnly; @@ -153,6 +156,7 @@ export default class extends Endpoint { // eslint- ...(typeof ps.allowRenoteToExternal === 'boolean' ? { allowRenoteToExternal: ps.allowRenoteToExternal } : {}), ...(ps.isLocalOnly !== undefined ? { isLocalOnly: ps.isLocalOnly } : {}), ...(ps.transferAdminUserId !== undefined && channel.userId === ps.transferAdminUserId ? { userId: ps.transferAdminUserId } : {}), + ...(validatedCollaboratorIds !== undefined ? { collaboratorIds: validatedCollaboratorIds } : {}), }); return await this.channelEntityService.pack(channel.id, me); diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index f6dcec46d56..3a5d2297474 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + {{ i18n.ts._channel.transferAdminConfirmTitle }} {{ i18n.ts.archive }} @@ -133,6 +133,7 @@ const allowRenoteToExternal = ref(true); const isLocalOnly = ref(false); const pinnedNoteIds = ref([]); const isRoot = ref(false); +const isOwner = ref(false); const collaboratorUsers = ref([]); watch(() => bannerId.value, async () => { @@ -172,6 +173,7 @@ async function fetchChannel() { collaboratorUsers.value = []; } isRoot.value = ($i && $i.id === result.userId) || iAmModerator; + isOwner.value = $i !== null && $i.id === result.userId; channel.value = result; } @@ -244,26 +246,29 @@ function collaboratorUserDelete(i: number) { } function save() { - const params = { - name: name.value, - description: description.value, - bannerId: bannerId.value, - color: color.value, - isSensitive: isSensitive.value, - allowRenoteToExternal: allowRenoteToExternal.value, - isLocalOnly: isLocalOnly.value, - collaboratorIds: collaboratorUsers.value.map(x => x.id), - } satisfies Misskey.entities.ChannelsCreateRequest; - if (props.channelId != null) { os.apiWithDialog('channels/update', { - ...params, channelId: props.channelId, + name: name.value, + description: description.value, + bannerId: bannerId.value, + color: color.value, + isSensitive: isSensitive.value, + allowRenoteToExternal: allowRenoteToExternal.value, + isLocalOnly: isLocalOnly.value, pinnedNoteIds: pinnedNoteIds.value, - collaboratorIds: collaboratorUsers.value.map(x => x.id), + ...(isRoot.value ? { collaboratorIds: collaboratorUsers.value.map(x => x.id) } : {}), }); } else { - os.apiWithDialog('channels/create', params).then(created => { + os.apiWithDialog('channels/create', { + name: name.value, + description: description.value, + bannerId: bannerId.value, + color: color.value, + isSensitive: isSensitive.value, + allowRenoteToExternal: allowRenoteToExternal.value, + isLocalOnly: isLocalOnly.value, + }).then(created => { router.push('/channels/:channelId', { params: { channelId: created.id, @@ -342,4 +347,28 @@ definePage(() => ({ margin: 0 8px; opacity: 0.5; } + +.userItem { + padding: 4px 0; +} + +.userItemMain { + display: flex; +} + +.userItemMainBody { + flex: 1; + min-width: 0; + margin-right: 8px; + + &:hover { + text-decoration: none; + } +} + +.unassign { + width: 32px; + height: 32px; + align-self: center; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 848d1c3e867..174d9d3b788 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -743,6 +743,9 @@ importers: frontend-shared: specifier: workspace:* version: link:../frontend-shared + horizon-sky: + specifier: 0.2.0 + version: 0.2.0 i18n: specifier: workspace:* version: link:../i18n @@ -827,6 +830,9 @@ importers: vue: specifier: 3.5.30 version: 3.5.30(typescript@5.9.3) + vuedraggable: + specifier: 2.24.3 + version: 2.24.3 wanakana: specifier: 5.3.1 version: 5.3.1 @@ -5002,6 +5008,7 @@ packages: '@xmldom/xmldom@0.9.8': resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} engines: {node: '>=14.6'} + deprecated: this version has critical issues, please update to the latest version abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} @@ -7049,6 +7056,9 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + horizon-sky@0.2.0: + resolution: {integrity: sha512-VHglmA15zeTQ5CjrGBKiidfgzR5La0FM5WamyMIwdtXjktQ6nNW+9dnKbtGdRsv4h1H4nbonRgYWPqmZEsWZcQ==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -9814,6 +9824,9 @@ packages: resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} engines: {node: '>=0.10.0'} + sortablejs@1.10.2: + resolution: {integrity: sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -10053,6 +10066,9 @@ packages: peerDependencies: postcss: ^8.4.32 + suncalc@1.9.0: + resolution: {integrity: sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A==} + superagent@10.3.0: resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} engines: {node: '>=14.18.0'} @@ -10761,6 +10777,9 @@ packages: vue-component-type-helpers@3.2.5: resolution: {integrity: sha512-tkvNr+bU8+xD/onAThIe7CHFvOJ/BO6XCOrxMzeytJq40nTfpGDJuVjyCM8ccGZKfAbGk2YfuZyDMXM56qheZQ==} + vue-component-type-helpers@3.2.6: + resolution: {integrity: sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -10802,6 +10821,9 @@ packages: typescript: optional: true + vuedraggable@2.24.3: + resolution: {integrity: sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -14365,7 +14387,7 @@ snapshots: storybook: 10.2.17(@testing-library/dom@10.4.0)(bufferutil@4.1.0)(prettier@3.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(utf-8-validate@6.0.6) type-fest: 2.19.0 vue: 3.5.30(typescript@5.9.3) - vue-component-type-helpers: 3.2.5 + vue-component-type-helpers: 3.2.6 '@stylistic/eslint-plugin@5.5.0(eslint@9.39.4)': dependencies: @@ -17917,6 +17939,10 @@ snapshots: highlight.js@11.11.1: {} + horizon-sky@0.2.0: + dependencies: + suncalc: 1.9.0 + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: @@ -21189,6 +21215,8 @@ snapshots: dependencies: is-plain-obj: 1.1.0 + sortablejs@1.10.2: {} + source-map-js@1.2.1: {} source-map-support@0.5.13: @@ -21455,6 +21483,8 @@ snapshots: postcss: 8.5.8 postcss-selector-parser: 7.1.1 + suncalc@1.9.0: {} + superagent@10.3.0: dependencies: component-emitter: 1.3.1 @@ -22092,6 +22122,8 @@ snapshots: vue-component-type-helpers@3.2.5: {} + vue-component-type-helpers@3.2.6: {} + vue-demi@0.14.10(vue@3.5.30(typescript@5.9.3)): dependencies: vue: 3.5.30(typescript@5.9.3) @@ -22144,6 +22176,10 @@ snapshots: optionalDependencies: typescript: 5.9.3 + vuedraggable@2.24.3: + dependencies: + sortablejs: 1.10.2 + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0