20
20
</ButtonStyled >
21
21
</div >
22
22
</div >
23
- <div
23
+ <ModerationProjectNags
24
24
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)
28
27
"
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
+ />
106
37
</template >
107
38
108
39
<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'
122
41
import { ButtonStyled , injectNotificationManager } from ' @modrinth/ui'
123
42
import type { Project , User , Version } from ' @modrinth/utils'
124
43
import { defineMessages , type MessageDescriptor , useVIntl } from ' @vintl/vintl'
125
- import type { Component } from ' vue'
126
44
import { computed } from ' vue'
127
45
128
46
import { acceptTeamInvite , removeTeamMember } from ' ~/helpers/teams.js'
129
47
48
+ import ModerationProjectNags from ' ./moderation/ModerationProjectNags.vue'
49
+
130
50
const { addNotification } = injectNotificationManager ()
131
51
132
52
interface Tags {
@@ -182,48 +102,6 @@ const messages = defineMessages({
182
102
id: ' project-member-header.decline' ,
183
103
defaultMessage: ' Decline' ,
184
104
},
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
- },
227
105
successJoin: {
228
106
id: ' project-member-header.success-join' ,
229
107
defaultMessage: ' You have joined the project team' ,
@@ -248,29 +126,10 @@ const messages = defineMessages({
248
126
id: ' project-member-header.error' ,
249
127
defaultMessage: ' Error' ,
250
128
},
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
- },
263
129
})
264
130
265
131
const { formatMessage } = useVIntl ()
266
132
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
-
274
133
function getFormattedMessage(message : string | MessageDescriptor ): string {
275
134
if (typeof message === ' string' ) {
276
135
return message
@@ -296,108 +155,6 @@ const emit = defineEmits<{
296
155
setProcessing: [processing : boolean ]
297
156
}>()
298
157
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
-
401
158
const showInvitation = computed <boolean >(() => {
402
159
if (props .allMembers && props .auth ) {
403
160
const member = props .allMembers .find ((x ) => x ?.user ?.id === props .auth .user .id )
@@ -472,9 +229,3 @@ async function declineInvite(): Promise<void> {
472
229
}
473
230
}
474
231
</script >
475
-
476
- <style lang="scss" scoped>
477
- .duration-250 {
478
- transition-duration : 250ms ;
479
- }
480
- </style >
0 commit comments