Files
AstralRinth/apps/frontend/src/components/ui/ProjectMemberHeader.vue
Jerozgen 5c00cb06f1 "Submit for review" button translation (#4381)
* "Submit for review" button translation

* Fix invitation message in the code

* Run web:fix

* Run intl:extract

---------

Co-authored-by: Calum H. <contact@cal.engineer>
2025-09-25 09:31:41 +00:00

233 lines
5.6 KiB
Vue

<template>
<div v-if="showInvitation" class="universal-card information invited my-4">
<h2>{{ getFormattedMessage(messages.invitationTitle) }}</h2>
<p v-if="currentMember?.project_role">
{{ formatMessage(messages.invitationWithRole, { role: currentMember.project_role }) }}
</p>
<p v-else>{{ getFormattedMessage(messages.invitationNoRole) }}</p>
<div class="input-group">
<ButtonStyled color="brand">
<button class="brand-button" @click="acceptInvite()">
<CheckIcon />
{{ getFormattedMessage(messages.accept) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button @click="declineInvite">
<XIcon />
{{ getFormattedMessage(messages.decline) }}
</button>
</ButtonStyled>
</div>
</div>
<ModerationProjectNags
v-if="
(currentMember && project.status === 'draft') ||
tags.rejectedStatuses.includes(project.status)
"
:project="project"
:versions="versions"
:current-member="currentMember"
:collapsed="collapsed"
:route-name="routeName"
:tags="tags"
@toggle-collapsed="handleToggleCollapsed"
@set-processing="handleSetProcessing"
/>
</template>
<script setup lang="ts">
import { CheckIcon, XIcon } from '@modrinth/assets'
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
import type { Project, User, Version } from '@modrinth/utils'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
import ModerationProjectNags from './moderation/ModerationProjectNags.vue'
const { addNotification } = injectNotificationManager()
interface Tags {
rejectedStatuses: string[]
}
interface Auth {
user: {
id: string
}
}
interface Member {
accepted?: boolean
project_role?: string
user?: Partial<User>
}
interface Props {
project: Project
versions?: Version[]
currentMember?: Member | null
allMembers?: Member[] | null
isSettings?: boolean
collapsed?: boolean
routeName?: string
auth: Auth
tags: Tags
setProcessing?: (processing: boolean) => void
toggleCollapsed?: () => void
updateMembers?: () => void | Promise<void>
}
const messages = defineMessages({
invitationTitle: {
id: 'project-member-header.invitation-title',
defaultMessage: 'Invitation to join project',
},
invitationWithRole: {
id: 'project-member-header.invitation-with-role',
defaultMessage:
"You've been invited to be a member of this project with the role of ''{role}''.",
},
invitationNoRole: {
id: 'project-member-header.invitation-no-role',
defaultMessage:
"You've been invited to join this project. Please accept or decline the invitation.",
},
accept: {
id: 'project-member-header.accept',
defaultMessage: 'Accept',
},
decline: {
id: 'project-member-header.decline',
defaultMessage: 'Decline',
},
successJoin: {
id: 'project-member-header.success-join',
defaultMessage: 'You have joined the project team',
},
errorJoin: {
id: 'project-member-header.error-join',
defaultMessage: 'Failed to accept team invitation',
},
successDecline: {
id: 'project-member-header.success-decline',
defaultMessage: 'You have declined the team invitation',
},
errorDecline: {
id: 'project-member-header.error-decline',
defaultMessage: 'Failed to decline team invitation',
},
success: {
id: 'project-member-header.success',
defaultMessage: 'Success',
},
error: {
id: 'project-member-header.error',
defaultMessage: 'Error',
},
})
const { formatMessage } = useVIntl()
function getFormattedMessage(message: string | MessageDescriptor): string {
if (typeof message === 'string') {
return message
}
return formatMessage(message)
}
const props = withDefaults(defineProps<Props>(), {
versions: () => [],
currentMember: null,
allMembers: null,
isSettings: false,
collapsed: false,
routeName: '',
setProcessing: () => {},
toggleCollapsed: () => {},
updateMembers: async () => {},
})
const emit = defineEmits<{
toggleCollapsed: []
updateMembers: []
setProcessing: [processing: boolean]
}>()
const showInvitation = computed<boolean>(() => {
if (props.allMembers && props.auth) {
const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id)
return !!member && !member.accepted
}
return false
})
function handleToggleCollapsed(): void {
if (props.toggleCollapsed) {
props.toggleCollapsed()
} else {
emit('toggleCollapsed')
}
}
async function handleUpdateMembers(): Promise<void> {
if (props.updateMembers) {
await props.updateMembers()
} else {
emit('updateMembers')
}
}
function handleSetProcessing(processing: boolean): void {
if (props.setProcessing) {
props.setProcessing(processing)
} else {
emit('setProcessing', processing)
}
}
async function acceptInvite(): Promise<void> {
try {
handleSetProcessing(true)
await acceptTeamInvite(props.project.team)
await handleUpdateMembers()
addNotification({
title: formatMessage(messages.success),
text: formatMessage(messages.successJoin),
type: 'success',
})
} catch {
addNotification({
title: formatMessage(messages.error),
text: formatMessage(messages.errorJoin),
type: 'error',
})
} finally {
handleSetProcessing(false)
}
}
async function declineInvite(): Promise<void> {
try {
handleSetProcessing(true)
await removeTeamMember(props.project.team, props.auth.user.id)
await handleUpdateMembers()
addNotification({
title: formatMessage(messages.success),
text: formatMessage(messages.successDecline),
type: 'success',
})
} catch {
addNotification({
title: formatMessage(messages.error),
text: formatMessage(messages.errorDecline),
type: 'error',
})
} finally {
handleSetProcessing(false)
}
}
</script>