Skip to content

Commit ab95dcf

Browse files
authored
refactor: move nags out of main project member header for perf (#4222)
1 parent ab539a3 commit ab95dcf

File tree

3 files changed

+330
-291
lines changed

3 files changed

+330
-291
lines changed

apps/frontend/src/components/ui/ProjectMemberHeader.vue

Lines changed: 15 additions & 264 deletions
Original file line numberDiff line numberDiff line change
@@ -20,113 +20,33 @@
2020
</ButtonStyled>
2121
</div>
2222
</div>
23-
<div
23+
<ModerationProjectNags
2424
v-if="
25-
currentMember &&
26-
visibleNags.length > 0 &&
27-
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
25+
(currentMember && project.status === 'draft') ||
26+
tags.rejectedStatuses.includes(project.status)
2827
"
29-
class="universal-card my-4"
30-
>
31-
<div class="flex max-w-full flex-wrap items-center gap-x-6 gap-y-4">
32-
<div class="flex flex-auto flex-wrap items-center gap-x-6 gap-y-4">
33-
<h2 class="my-0 mr-auto">
34-
{{ getFormattedMessage(messages.publishingChecklist) }}
35-
</h2>
36-
<div class="flex flex-row gap-2">
37-
<div class="flex items-center gap-1">
38-
<AsteriskIcon class="size-4 text-red" />
39-
<span class="text-secondary">{{ getFormattedMessage(messages.required) }}</span>
40-
</div>
41-
|
42-
<div class="flex items-center gap-1">
43-
<TriangleAlertIcon class="size-4 text-orange" />
44-
<span class="text-secondary">{{ getFormattedMessage(messages.warning) }}</span>
45-
</div>
46-
|
47-
<div class="flex items-center gap-1">
48-
<LightBulbIcon class="size-4 text-purple" />
49-
<span class="text-secondary">{{ getFormattedMessage(messages.suggestion) }}</span>
50-
</div>
51-
</div>
52-
</div>
53-
<div class="input-group">
54-
<ButtonStyled circular>
55-
<button :class="!collapsed && '[&>svg]:rotate-180'" @click="handleToggleCollapsed()">
56-
<DropdownIcon class="duration-250 transition-transform ease-in-out" />
57-
</button>
58-
</ButtonStyled>
59-
</div>
60-
</div>
61-
<div v-if="!collapsed" class="grid-display width-16 mt-4">
62-
<div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
63-
<span class="flex items-center gap-2 font-semibold">
64-
<component
65-
:is="nag.icon || getDefaultIcon(nag.status)"
66-
v-tooltip="getStatusTooltip(nag.status)"
67-
:class="[
68-
'size-4',
69-
nag.status === 'required' && 'text-red',
70-
nag.status === 'warning' && 'text-orange',
71-
nag.status === 'suggestion' && 'text-purple',
72-
]"
73-
:aria-label="getStatusTooltip(nag.status)"
74-
/>
75-
{{ getFormattedMessage(nag.title) }}
76-
</span>
77-
{{ getNagDescription(nag) }}
78-
<NuxtLink
79-
v-if="nag.link && shouldShowLink(nag)"
80-
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
81-
nag.link.path
82-
}`"
83-
class="goto-link"
84-
>
85-
{{ getFormattedMessage(nag.link.title) }}
86-
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
87-
</NuxtLink>
88-
<ButtonStyled
89-
v-if="nag.status === 'special-submit-action' && nag.id === 'submit-for-review'"
90-
color="orange"
91-
@click="submitForReview"
92-
>
93-
<button
94-
v-tooltip="
95-
!canSubmitForReview ? getFormattedMessage(messages.submitChecklistTooltip) : undefined
96-
"
97-
:disabled="!canSubmitForReview"
98-
>
99-
<SendIcon />
100-
{{ getFormattedMessage(messages.submitForReview) }}
101-
</button>
102-
</ButtonStyled>
103-
</div>
104-
</div>
105-
</div>
28+
:project="project"
29+
:versions="versions"
30+
:current-member="currentMember"
31+
:collapsed="collapsed"
32+
:route-name="routeName"
33+
:tags="tags"
34+
@toggle-collapsed="handleToggleCollapsed"
35+
@set-processing="handleSetProcessing"
36+
/>
10637
</template>
10738

10839
<script setup lang="ts">
109-
import {
110-
AsteriskIcon,
111-
CheckIcon,
112-
ChevronRightIcon,
113-
DropdownIcon,
114-
LightBulbIcon,
115-
ScaleIcon,
116-
SendIcon,
117-
TriangleAlertIcon,
118-
XIcon,
119-
} from '@modrinth/assets'
120-
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
121-
import { nags } from '@modrinth/moderation'
40+
import { CheckIcon, XIcon } from '@modrinth/assets'
12241
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
12342
import type { Project, User, Version } from '@modrinth/utils'
12443
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
125-
import type { Component } from 'vue'
12644
import { computed } from 'vue'
12745
12846
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
12947
48+
import ModerationProjectNags from './moderation/ModerationProjectNags.vue'
49+
13050
const { addNotification } = injectNotificationManager()
13151
13252
interface Tags {
@@ -182,48 +102,6 @@ const messages = defineMessages({
182102
id: 'project-member-header.decline',
183103
defaultMessage: 'Decline',
184104
},
185-
publishingChecklist: {
186-
id: 'project-member-header.publishing-checklist',
187-
defaultMessage: 'Publishing checklist',
188-
},
189-
submitForReview: {
190-
id: 'project-member-header.submit-for-review',
191-
defaultMessage: 'Submit for review',
192-
},
193-
submitForReviewDesc: {
194-
id: 'project-member-header.submit-for-review-desc',
195-
defaultMessage:
196-
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
197-
},
198-
resubmitForReview: {
199-
id: 'project-member-header.resubmit-for-review',
200-
defaultMessage: 'Resubmit for review',
201-
},
202-
resubmitForReviewDesc: {
203-
id: 'project-member-header.resubmit-for-review-desc',
204-
defaultMessage:
205-
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
206-
},
207-
showKey: {
208-
id: 'project-member-header.show-key',
209-
defaultMessage: 'Toggle key',
210-
},
211-
keyTitle: {
212-
id: 'project-member-header.key-title',
213-
defaultMessage: 'Status Key',
214-
},
215-
action: {
216-
id: 'project-member-header.action',
217-
defaultMessage: 'Action',
218-
},
219-
visitModerationPage: {
220-
id: 'project-member-header.visit-moderation-page',
221-
defaultMessage: 'Visit moderation page',
222-
},
223-
submitChecklistTooltip: {
224-
id: 'project-member-header.submit-checklist-tooltip',
225-
defaultMessage: 'You must complete the required steps in the publishing checklist!',
226-
},
227105
successJoin: {
228106
id: 'project-member-header.success-join',
229107
defaultMessage: 'You have joined the project team',
@@ -248,29 +126,10 @@ const messages = defineMessages({
248126
id: 'project-member-header.error',
249127
defaultMessage: 'Error',
250128
},
251-
required: {
252-
id: 'project-member-header.required',
253-
defaultMessage: 'Required',
254-
},
255-
warning: {
256-
id: 'project-member-header.warning',
257-
defaultMessage: 'Warning',
258-
},
259-
suggestion: {
260-
id: 'project-member-header.suggestion',
261-
defaultMessage: 'Suggestion',
262-
},
263129
})
264130
265131
const { formatMessage } = useVIntl()
266132
267-
function getNagDescription(nag: Nag): string {
268-
if (typeof nag.description === 'function') {
269-
return nag.description(nagContext.value)
270-
}
271-
return formatMessage(nag.description)
272-
}
273-
274133
function getFormattedMessage(message: string | MessageDescriptor): string {
275134
if (typeof message === 'string') {
276135
return message
@@ -296,108 +155,6 @@ const emit = defineEmits<{
296155
setProcessing: [processing: boolean]
297156
}>()
298157
299-
const nagContext = computed<NagContext>(() => ({
300-
project: props.project,
301-
versions: props.versions,
302-
currentMember: props.currentMember as User,
303-
currentRoute: props.routeName,
304-
tags: props.tags,
305-
submitProject: submitForReview,
306-
}))
307-
308-
const canSubmitForReview = computed(() => {
309-
return (
310-
applicableNags.value.filter((nag) => nag.status === 'required' && !isNagComplete(nag))
311-
.length === 0
312-
)
313-
})
314-
315-
async function submitForReview() {
316-
if (canSubmitForReview.value) {
317-
await handleSetProcessing(true)
318-
}
319-
}
320-
321-
const applicableNags = computed<Nag[]>(() => {
322-
return nags.filter((nag) => {
323-
return nag.shouldShow(nagContext.value)
324-
})
325-
})
326-
327-
function isNagComplete(nag: Nag): boolean {
328-
const context = nagContext.value
329-
return !nag.shouldShow(context)
330-
}
331-
332-
const visibleNags = computed<Nag[]>(() => {
333-
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag))
334-
335-
if (props.project.status === 'draft') {
336-
finalNags.push({
337-
id: 'submit-for-review',
338-
title: messages.submitForReview,
339-
description: () => formatMessage(messages.submitForReviewDesc),
340-
status: 'special-submit-action',
341-
shouldShow: (ctx) => ctx.project.status === 'draft',
342-
})
343-
}
344-
345-
if (props.tags.rejectedStatuses.includes(props.project.status)) {
346-
finalNags.push({
347-
id: 'resubmit-for-review',
348-
title: messages.resubmitForReview,
349-
description: (ctx) =>
350-
formatMessage(messages.resubmitForReviewDesc, { status: ctx.project.status }),
351-
status: 'special-submit-action',
352-
shouldShow: (ctx) => ctx.tags.rejectedStatuses.includes(ctx.project.status),
353-
link: {
354-
path: 'moderation',
355-
title: messages.visitModerationPage,
356-
shouldShow: () => props.routeName !== 'type-id-moderation',
357-
},
358-
})
359-
}
360-
361-
finalNags.sort((a, b) => {
362-
const statusOrder = { required: 0, warning: 1, suggestion: 2, 'special-submit-action': 3 }
363-
return statusOrder[a.status] - statusOrder[b.status]
364-
})
365-
366-
return finalNags
367-
})
368-
369-
function shouldShowLink(nag: Nag): boolean {
370-
return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false
371-
}
372-
373-
function getDefaultIcon(status: NagStatus): Component {
374-
switch (status) {
375-
case 'required':
376-
return AsteriskIcon
377-
case 'warning':
378-
return TriangleAlertIcon
379-
case 'suggestion':
380-
return LightBulbIcon
381-
case 'special-submit-action':
382-
return ScaleIcon
383-
default:
384-
return AsteriskIcon
385-
}
386-
}
387-
388-
function getStatusTooltip(status: NagStatus): string {
389-
switch (status) {
390-
case 'required':
391-
return formatMessage(messages.required)
392-
case 'warning':
393-
return formatMessage(messages.warning)
394-
case 'suggestion':
395-
return formatMessage(messages.suggestion)
396-
default:
397-
return formatMessage(messages.required)
398-
}
399-
}
400-
401158
const showInvitation = computed<boolean>(() => {
402159
if (props.allMembers && props.auth) {
403160
const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id)
@@ -472,9 +229,3 @@ async function declineInvite(): Promise<void> {
472229
}
473230
}
474231
</script>
475-
476-
<style lang="scss" scoped>
477-
.duration-250 {
478-
transition-duration: 250ms;
479-
}
480-
</style>

0 commit comments

Comments
 (0)