Fix org ownership (#1553)

This commit is contained in:
Geometrically
2024-01-10 15:13:37 -05:00
committed by GitHub
parent 354bfe58cd
commit 5924154a62
5 changed files with 98 additions and 71 deletions

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-crown"><path d="m2 4 3 12h14l3-12-6 7-4-7-4 7-6-7zm3 16h14"/></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@@ -637,7 +637,9 @@
> >
<Avatar :src="organization.icon_url" :alt="organization.name" size="sm" /> <Avatar :src="organization.icon_url" :alt="organization.name" size="sm" />
<div class="member-info"> <div class="member-info">
<p class="name">{{ organization.name }}</p> <p class="name">
{{ organization.name }}
</p>
<p class="role"><OrganizationIcon /> Organization</p> <p class="role"><OrganizationIcon /> Organization</p>
</div> </div>
</nuxt-link> </nuxt-link>
@@ -650,7 +652,9 @@
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle /> <Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
<div class="member-info"> <div class="member-info">
<p class="name">{{ member.name }}</p> <p class="name">
{{ member.name }} <CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
</p>
<p class="role"> <p class="role">
{{ member.role }} {{ member.role }}
</p> </p>
@@ -766,6 +770,7 @@ import {
Checkbox, Checkbox,
ChartIcon, ChartIcon,
} from 'omorphia' } from 'omorphia'
import CrownIcon from '~/assets/images/utils/crown.svg'
import CalendarIcon from '~/assets/images/utils/calendar.svg' import CalendarIcon from '~/assets/images/utils/calendar.svg'
import ClearIcon from '~/assets/images/utils/clear.svg' import ClearIcon from '~/assets/images/utils/clear.svg'
import DownloadIcon from '~/assets/images/utils/download.svg' import DownloadIcon from '~/assets/images/utils/download.svg'
@@ -878,7 +883,7 @@ try {
}), }),
useAsyncData( useAsyncData(
`project/${route.params.id}/members`, `project/${route.params.id}/members`,
() => useBaseFetch(`project/${route.params.id}/members`), () => useBaseFetch(`project/${route.params.id}/members`, { apiVersion: 3 }),
{ {
transform: (members) => { transform: (members) => {
members.forEach((it, index) => { members.forEach((it, index) => {
@@ -939,8 +944,8 @@ if (project.value.project_type !== route.params.type || route.params.id !== proj
// The rest of the members should be sorted by role, then by name // The rest of the members should be sorted by role, then by name
const members = computed(() => { const members = computed(() => {
const acceptedMembers = allMembers.value.filter((x) => x.accepted) const acceptedMembers = allMembers.value.filter((x) => x.accepted)
const owner = acceptedMembers.find((x) => x.role === 'Owner') const owner = acceptedMembers.find((x) => x.is_owner)
const rest = acceptedMembers.filter((x) => x.role !== 'Owner') || [] const rest = acceptedMembers.filter((x) => !x.is_owner) || []
rest.sort((a, b) => { rest.sort((a, b) => {
if (a.role === b.role) { if (a.role === b.role) {
@@ -1017,9 +1022,7 @@ const description = computed(
() => () =>
`${project.value.description} - Download the Minecraft ${projectTypeDisplay.value} ${ `${project.value.description} - Download the Minecraft ${projectTypeDisplay.value} ${
project.value.title project.value.title
} by ${ } by ${members.value.find((x) => x.is_owner)?.user?.username || 'a Creator'} on Modrinth`
members.value.find((x) => x.role === 'Owner')?.user?.username || 'a Creator'
} on Modrinth`
) )
if (!route.name.startsWith('type-id-settings')) { if (!route.name.startsWith('type-id-settings')) {
@@ -1409,6 +1412,13 @@ const collapsedChecklist = ref(false)
.name { .name {
font-weight: bold; font-weight: bold;
display: flex;
align-items: center;
gap: 0.25rem;
svg {
color: var(--color-orange);
}
} }
p { p {

View File

@@ -47,7 +47,7 @@
</span> </span>
<button <button
class="iconified-button danger-button" class="iconified-button danger-button"
:disabled="props.currentMember?.role === 'Owner'" :disabled="props.currentMember?.is_owner"
@click="leaveProject()" @click="leaveProject()"
> >
<UserRemoveIcon /> <UserRemoveIcon />
@@ -67,6 +67,7 @@
<div class="text"> <div class="text">
<nuxt-link :to="'/user/' + member.user.username" class="name"> <nuxt-link :to="'/user/' + member.user.username" class="name">
<p>{{ member.name }}</p> <p>{{ member.name }}</p>
<CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
</nuxt-link> </nuxt-link>
<p>{{ member.role }}</p> <p>{{ member.role }}</p>
</div> </div>
@@ -87,7 +88,7 @@
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div v-if="member.oldRole !== 'Owner'" class="adjacent-input"> <div class="adjacent-input">
<label :for="`member-${allTeamMembers[index].user.username}-role`"> <label :for="`member-${allTeamMembers[index].user.username}-role`">
<span class="label__title">Role</span> <span class="label__title">Role</span>
<span class="label__description"> <span class="label__description">
@@ -98,7 +99,6 @@
:id="`member-${allTeamMembers[index].user.username}-role`" :id="`member-${allTeamMembers[index].user.username}-role`"
v-model="allTeamMembers[index].role" v-model="allTeamMembers[index].role"
type="text" type="text"
:class="{ 'known-error': member.role === 'Owner' }"
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER" :disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
/> />
</div> </div>
@@ -117,11 +117,7 @@
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER" :disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
/> />
</div> </div>
<p v-if="member.role === 'Owner' && member.oldRole !== 'Owner'" class="known-errors"> <template v-if="!member.is_owner">
A project can only have one 'Owner'. Use the 'Transfer ownership' button below if you no
longer wish to be owner.
</p>
<template v-if="member.oldRole !== 'Owner'">
<span class="label"> <span class="label">
<span class="label__title">Permissions</span> <span class="label__title">Permissions</span>
</span> </span>
@@ -225,7 +221,7 @@
Save changes Save changes
</button> </button>
<button <button
v-if="member.oldRole !== 'Owner'" v-if="!member.is_owner"
class="iconified-button danger-button" class="iconified-button danger-button"
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER" :disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
@click="removeTeamMember(index)" @click="removeTeamMember(index)"
@@ -234,9 +230,7 @@
Remove member Remove member
</button> </button>
<button <button
v-if=" v-if="!member.is_owner && props.currentMember?.is_owner && member.accepted"
member.oldRole !== 'Owner' && props.currentMember?.role === 'Owner' && member.accepted
"
class="iconified-button" class="iconified-button"
@click="transferOwnership(index)" @click="transferOwnership(index)"
> >
@@ -300,7 +294,7 @@
:show-labels="false" :show-labels="false"
:allow-empty="false" :allow-empty="false"
:options="organizations || []" :options="organizations || []"
:disabled="props.currentMember?.role !== 'Owner' || organizations?.length === 0" :disabled="!props.currentMember?.is_owner || organizations?.length === 0"
/> />
<button class="btn btn-primary" :disabled="!selectedOrganization" @click="onAddToOrg"> <button class="btn btn-primary" :disabled="!selectedOrganization" @click="onAddToOrg">
<CheckIcon /> <CheckIcon />
@@ -324,6 +318,7 @@
<div class="text"> <div class="text">
<nuxt-link :to="'/user/' + member.user.username" class="name"> <nuxt-link :to="'/user/' + member.user.username" class="name">
<p>{{ member.user.username }}</p> <p>{{ member.user.username }}</p>
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
</nuxt-link> </nuxt-link>
<p>{{ member.role }}</p> <p>{{ member.role }}</p>
</div> </div>
@@ -344,7 +339,7 @@
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div v-if="!member.is_owner" class="adjacent-input"> <div class="adjacent-input">
<label :for="`member-${allOrgMembers[index].user.username}-override-perms`"> <label :for="`member-${allOrgMembers[index].user.username}-override-perms`">
<span class="label__title">Override values</span> <span class="label__title">Override values</span>
<span class="label__description"> <span class="label__description">
@@ -529,6 +524,7 @@ import SaveIcon from '~/assets/images/utils/save.svg'
import UserPlusIcon from '~/assets/images/utils/user-plus.svg' import UserPlusIcon from '~/assets/images/utils/user-plus.svg'
import UserRemoveIcon from '~/assets/images/utils/user-x.svg' import UserRemoveIcon from '~/assets/images/utils/user-x.svg'
import OrganizationIcon from '~/assets/images/utils/organization.svg' import OrganizationIcon from '~/assets/images/utils/organization.svg'
import CrownIcon from '~/assets/images/utils/crown.svg'
import { removeSelfFromTeam } from '~/helpers/teams.js' import { removeSelfFromTeam } from '~/helpers/teams.js'
@@ -600,9 +596,9 @@ function initMembers() {
allOrgMembers.value = selectedMembersForOrg allOrgMembers.value = selectedMembersForOrg
allTeamMembers.value = props.allMembers allTeamMembers.value = props.allMembers.filter(
.map((x) => ({ ...x, oldRole: x.role })) (x) => !selectedMembersForOrg.some((y) => y.user.id === x.user.id)
.filter((x) => !selectedMembersForOrg.some((y) => y.user.id === x.user.id)) )
} }
watch( watch(
@@ -737,16 +733,16 @@ const updateTeamMember = async (index) => {
startLoading() startLoading()
try { try {
const data = const data = !allTeamMembers.value[index].is_owner
allTeamMembers.value[index].oldRole !== 'Owner' ? {
? { permissions: allTeamMembers.value[index].permissions,
permissions: allTeamMembers.value[index].permissions, role: allTeamMembers.value[index].role,
role: allTeamMembers.value[index].role, payouts_split: allTeamMembers.value[index].payouts_split,
payouts_split: allTeamMembers.value[index].payouts_split, }
} : {
: { payouts_split: allTeamMembers.value[index].payouts_split,
payouts_split: allTeamMembers.value[index].payouts_split, role: allTeamMembers.value[index].role,
} }
await useBaseFetch( await useBaseFetch(
`team/${props.project.team}/members/${allTeamMembers.value[index].user.id}`, `team/${props.project.team}/members/${allTeamMembers.value[index].user.id}`,
@@ -900,6 +896,14 @@ const updateMembers = async () => {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
.name { .name {
font-weight: bold; font-weight: bold;
display: flex;
align-items: center;
gap: 0.25rem;
svg {
color: var(--color-orange);
}
} }
p { p {
margin: 0.2rem 0; margin: 0.2rem 0;

View File

@@ -107,7 +107,10 @@
<template v-for="member in acceptedMembers" :key="member.user.id"> <template v-for="member in acceptedMembers" :key="member.user.id">
<nuxt-link class="creator button-base" :to="`/user/${member.user.username}`"> <nuxt-link class="creator button-base" :to="`/user/${member.user.username}`">
<Avatar :src="member.user.avatar_url" circle /> <Avatar :src="member.user.avatar_url" circle />
<p class="name">{{ member.user.username }}</p> <p class="name">
{{ member.user.username }}
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
</p>
<p class="role">{{ member.role }}</p> <p class="role">{{ member.role }}</p>
</nuxt-link> </nuxt-link>
</template> </template>
@@ -226,6 +229,7 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg'
import ProjectCard from '~/components/ui/ProjectCard.vue' import ProjectCard from '~/components/ui/ProjectCard.vue'
import OrganizationIcon from '~/assets/images/utils/organization.svg' import OrganizationIcon from '~/assets/images/utils/organization.svg'
import CrownIcon from '~/assets/images/utils/crown.svg'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js' import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
const vintl = useVIntl() const vintl = useVIntl()
@@ -269,8 +273,8 @@ if (!organization.value) {
// Filter accepted, sort by role, then by name and Owner role always goes first // Filter accepted, sort by role, then by name and Owner role always goes first
const acceptedMembers = computed(() => { const acceptedMembers = computed(() => {
const acceptedMembers = organization.value.members?.filter((x) => x.accepted) const acceptedMembers = organization.value.members?.filter((x) => x.accepted)
const owner = acceptedMembers.find((x) => x.role === 'Owner') const owner = acceptedMembers.find((x) => x.is_owner)
const rest = acceptedMembers.filter((x) => x.role !== 'Owner') || [] const rest = acceptedMembers.filter((x) => !x.is_owner) || []
rest.sort((a, b) => { rest.sort((a, b) => {
if (a.role === b.role) { if (a.role === b.role) {
@@ -446,6 +450,14 @@ useSeoMeta({
align-self: flex-end; align-self: flex-end;
margin-left: var(--gap-xs); margin-left: var(--gap-xs);
font-weight: bold; font-weight: bold;
display: flex;
align-items: center;
gap: 0.25rem;
svg {
color: var(--color-orange);
}
} }
.role { .role {

View File

@@ -51,7 +51,7 @@
</span> </span>
<Button <Button
color="danger" color="danger"
:disabled="currentMember.role === 'Owner'" :disabled="currentMember.is_owner"
@click="() => onLeaveProject(organization.team_id, auth.user.id)" @click="() => onLeaveProject(organization.team_id, auth.user.id)"
> >
<UserRemoveIcon /> <UserRemoveIcon />
@@ -71,6 +71,7 @@
<div class="text"> <div class="text">
<nuxt-link :to="'/user/' + member.user.username" class="name"> <nuxt-link :to="'/user/' + member.user.username" class="name">
<p>{{ member.user.username }}</p> <p>{{ member.user.username }}</p>
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
</nuxt-link> </nuxt-link>
<p>{{ member.role }}</p> <p>{{ member.role }}</p>
</div> </div>
@@ -93,7 +94,7 @@
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div v-if="member.oldRole !== 'Owner'" class="adjacent-input"> <div class="adjacent-input">
<label :for="`member-${member.user.id}-role`"> <label :for="`member-${member.user.id}-role`">
<span class="label__title">Role</span> <span class="label__title">Role</span>
<span class="label__description"> <span class="label__description">
@@ -104,7 +105,6 @@
:id="`member-${member.user.id}-role`" :id="`member-${member.user.id}-role`"
v-model="member.role" v-model="member.role"
type="text" type="text"
:class="{ 'known-error': member.role === 'Owner' }"
:disabled=" :disabled="
!isPermission( !isPermission(
currentMember.organization_permissions, currentMember.organization_permissions,
@@ -133,11 +133,7 @@
" "
/> />
</div> </div>
<p v-if="member.role === 'Owner' && member.oldRole !== 'Owner'" class="known-errors"> <template v-if="!member.is_owner">
A organization can only have one 'Owner'. Use the 'Transfer ownership' button below if you
no longer wish to be owner.
</p>
<template v-if="member.oldRole !== 'Owner'">
<span class="label"> <span class="label">
<span class="label__title">Project permissions</span> <span class="label__title">Project permissions</span>
</span> </span>
@@ -157,7 +153,7 @@
/> />
</div> </div>
</template> </template>
<template v-if="member.oldRole !== 'Owner'"> <template v-if="!member.is_owner">
<span class="label"> <span class="label">
<span class="label__title">Organization permissions</span> <span class="label__title">Organization permissions</span>
</span> </span>
@@ -192,7 +188,7 @@
Save changes Save changes
</Button> </Button>
<Button <Button
v-if="member.oldRole !== 'Owner'" v-if="!member.is_owner"
color="danger" color="danger"
:disabled=" :disabled="
!isPermission( !isPermission(
@@ -210,7 +206,7 @@
Remove member Remove member
</Button> </Button>
<Button <Button
v-if="member.oldRole !== 'Owner' && currentMember.role === 'Owner' && member.accepted" v-if="!member.is_owner && currentMember.is_owner && member.accepted"
@click="() => onTransferOwnership(organization.team_id, member.user.id)" @click="() => onTransferOwnership(organization.team_id, member.user.id)"
> >
<TransferIcon /> <TransferIcon />
@@ -234,8 +230,9 @@ import {
DropdownIcon, DropdownIcon,
Button, Button,
} from 'omorphia' } from 'omorphia'
import { ref } from 'vue' import { ref } from 'vue'
import CrownIcon from '~/assets/images/utils/crown.svg'
import { removeTeamMember } from '~/helpers/teams.js' import { removeTeamMember } from '~/helpers/teams.js'
import { isPermission } from '~/utils/permissions.ts' import { isPermission } from '~/utils/permissions.ts'
@@ -246,20 +243,13 @@ const auth = await useAuth()
const currentUsername = ref('') const currentUsername = ref('')
const openTeamMembers = ref([]) const openTeamMembers = ref([])
const processMembers = (members) => { const allTeamMembers = ref(organization.value.members)
return members
.map((x) => ({ ...x, oldRole: x.role }))
.sort((a, b) => a.user.username.localeCompare(b.user.username))
}
const allTeamMembers = ref(processMembers(organization.value.members))
watch( watch(
() => organization.value, () => organization.value,
() => { () => {
allTeamMembers.value = processMembers(organization.value.members) allTeamMembers.value = organization.value.members
}, }
{ deep: true, immediate: true }
) )
const projectPermissions = { const projectPermissions = {
@@ -329,17 +319,17 @@ const onRemoveMember = useClientTry(async (teamId, member) => {
}) })
const onUpdateTeamMember = useClientTry(async (teamId, member) => { const onUpdateTeamMember = useClientTry(async (teamId, member) => {
const data = const data = !member.is_owner
member.oldRole !== 'Owner' ? {
? { permissions: member.permissions,
permissions: member.permissions, organization_permissions: member.organization_permissions,
organization_permissions: member.organization_permissions, role: member.role,
role: member.role, payouts_split: member.payouts_split,
payouts_split: member.payouts_split, }
} : {
: { payouts_split: member.payouts_split,
payouts_split: member.payouts_split, role: member.role,
} }
await useBaseFetch(`team/${teamId}/members/${member.user.id}`, { await useBaseFetch(`team/${teamId}/members/${member.user.id}`, {
method: 'PATCH', method: 'PATCH',
body: data, body: data,
@@ -381,9 +371,19 @@ const onTransferOwnership = useClientTry(async (teamId, uid) => {
.text { .text {
margin: auto 0 auto 0.5rem; margin: auto 0 auto 0.5rem;
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
.name { .name {
font-weight: bold; font-weight: bold;
display: flex;
align-items: center;
gap: 0.25rem;
svg {
color: var(--color-orange);
}
} }
p { p {
margin: 0.2rem 0; margin: 0.2rem 0;
} }