New organizations (#1488)

* [WIP] Transfer organizations to own branch

* push progress

* Setup organizations page

* Add organizations grid to user profile

* Remove debug

* Add error handling for failed organization fetch

* Refactor organization page and settings

* Restructure to composition setup api

* checklist completion

* Apply suggestions from code review

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Update pages/[type]/[id]/settings/index.vue

Co-authored-by: Emma Alexia <emma@modrinth.com>

* Clean up org state management

* Refactor useClientTry to simplify code

* Remove unused code and update dependencies

* Refactor bulkEditLinks event handler

* Refactor organization management functions

* Update heading from "Creators" to "Members"

* Refactor team member invitation

* Refactor member management functions

* Implement validation on clientside for org names

* Name sanitization for fun characters

* Update onInviteTeamMember function parameters

* Remove name

* sidebar

* random rendering issue

* Conform to org removal

* Org no projects conditional

* Update organization links in dashboard

* Update Cards to universal-cards

* Refactor gallery upload permissions

* Refactor to sidebar pattern

* Update button classes in gallery and versions components

* Finish (most)

* almost finish

* Finish orgs :D

* Fix lint

* orgs fixes

* fix most things

* project settings

* convert grid to cards

* clean up unused test class

* Settings -> Manage

* add org view to org management

* Fix prop mounting issue

* fix analytics grid layout overflow

* fix multiselect breaking layout

* Refactor chart selection logic in ChartDisplay.vue

* Add transfer modal

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Emma Alexia <emma@modrinth.com>
This commit is contained in:
Carter
2024-01-06 15:09:26 -08:00
committed by GitHub
parent 1108b0264e
commit d893765b24
44 changed files with 4092 additions and 1037 deletions

View File

@@ -54,7 +54,11 @@
</div>
</div>
<div v-if="!collapsed" class="grid-display width-16">
<div v-for="nag in nags.filter((x) => x.condition)" :key="nag.id" class="grid-display__item">
<div
v-for="nag in nags.filter((x) => x.condition && !x.hide)"
:key="nag.id"
class="grid-display__item"
>
<span class="label">
<RequiredIcon
v-if="nag.status === 'required'"
@@ -101,7 +105,9 @@
</div>
</template>
<script>
<script setup>
import { formatProjectType } from '~/plugins/shorthands.js'
import ChevronRightIcon from '~/assets/images/utils/chevron-right.svg'
import DropdownIcon from '~/assets/images/utils/dropdown.svg'
import CheckIcon from '~/assets/images/utils/check.svg'
@@ -110,309 +116,266 @@ import RequiredIcon from '~/assets/images/utils/asterisk.svg'
import SuggestionIcon from '~/assets/images/utils/lightbulb.svg'
import ModerationIcon from '~/assets/images/sidebar/admin.svg'
import SendIcon from '~/assets/images/utils/send.svg'
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams.js'
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
export default {
components: {
ChevronRightIcon,
DropdownIcon,
CheckIcon,
CrossIcon,
RequiredIcon,
SuggestionIcon,
ModerationIcon,
SendIcon,
const props = defineProps({
project: {
type: Object,
required: true,
},
props: {
project: {
type: Object,
required: true,
},
versions: {
type: Array,
default() {
return []
},
},
currentMember: {
type: Object,
default: null,
},
allMembers: {
type: Object,
default: null,
},
isSettings: {
type: Boolean,
default: false,
},
collapsed: {
type: Boolean,
default: false,
},
routeName: {
type: String,
default: '',
},
auth: {
type: Object,
required: true,
},
tags: {
type: Object,
required: true,
},
setProcessing: {
type: Function,
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'setProcessing function not found',
type: 'error',
})
}
},
},
toggleCollapsed: {
type: Function,
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'toggleCollapsed function not found',
type: 'error',
})
}
},
},
updateMembers: {
type: Function,
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'updateMembers function not found',
type: 'error',
})
}
},
versions: {
type: Array,
default() {
return []
},
},
computed: {
featuredGalleryImage() {
return this.project.gallery.find((img) => img.featured)
currentMember: {
type: Object,
default: null,
},
allMembers: {
type: Object,
default: null,
},
isSettings: {
type: Boolean,
default: false,
},
collapsed: {
type: Boolean,
default: false,
},
routeName: {
type: String,
default: '',
},
auth: {
type: Object,
required: true,
},
tags: {
type: Object,
required: true,
},
setProcessing: {
type: Function,
default() {
return () => {
addNotification({
group: 'main',
title: 'An error occurred',
text: 'setProcessing function not found',
type: 'error',
})
}
},
nags() {
return [
{
condition:
this.project.body === '' || this.project.body.startsWith('# Placeholder description'),
title: 'Add a description',
id: 'add-description',
description:
"A description that clearly describes the project's purpose and function is required.",
status: 'required',
link: {
path: 'settings/description',
title: 'Visit description settings',
hide: this.routeName === 'type-id-settings-description',
},
},
{
condition: !this.project.icon_url,
title: 'Add an icon',
id: 'add-icon',
description:
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
status: 'suggestion',
link: {
path: 'settings',
title: 'Visit general settings',
hide: this.routeName === 'type-id-settings',
},
},
{
condition: !this.featuredGalleryImage,
title: 'Feature a gallery image',
id: 'feature-gallery-image',
description: 'Featured gallery images may be the first impression of many users.',
status: 'suggestion',
link: {
path: 'gallery',
title: 'Visit gallery page',
hide: this.routeName === 'type-id-gallery',
},
},
{
condition: this.versions.length < 1,
title: 'Upload a version',
id: 'upload-version',
description: 'At least one version is required for a project to be submitted for review.',
status: 'required',
link: {
path: 'versions',
title: 'Visit versions page',
hide: this.routeName === 'type-id-versions',
},
},
{
condition: this.project.categories.length < 1,
title: 'Select tags',
id: 'select-tags',
description: 'Select all tags that apply to your project.',
status: 'suggestion',
link: {
path: 'settings/tags',
title: 'Visit tag settings',
hide: this.routeName === 'type-id-settings-tags',
},
},
{
condition: !(
this.project.issues_url ||
this.project.source_url ||
this.project.wiki_url ||
this.project.discord_url ||
this.project.donation_urls.length > 0
),
title: 'Add external links',
id: 'add-links',
description:
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
status: 'suggestion',
link: {
path: 'settings/links',
title: 'Visit links settings',
hide: this.routeName === 'type-id-settings-links',
},
},
{
hide:
this.project.project_type === 'resourcepack' ||
this.project.project_type === 'plugin' ||
this.project.project_type === 'shader' ||
this.project.project_type === 'datapack',
condition:
this.project.client_side === 'unknown' || this.project.server_side === 'unknown',
title: 'Select supported environments',
id: 'select-environments',
description: `Select if the ${this.$formatProjectType(
this.project.project_type
).toLowerCase()} functions on the client-side and/or server-side.`,
status: 'required',
link: {
path: 'settings',
title: 'Visit general settings',
hide: this.routeName === 'type-id-settings',
},
},
{
condition: this.project.license.id === 'LicenseRef-Unknown',
title: 'Select license',
id: 'select-license',
description: `Select the license your ${this.$formatProjectType(
this.project.project_type
).toLowerCase()} is distributed under.`,
status: 'required',
link: {
path: 'settings/license',
title: 'Visit license settings',
hide: this.routeName === 'type-id-settings-license',
},
},
{
hide: this.project.status !== 'draft',
condition: true,
title: 'Submit for review',
id: 'submit-for-review',
description:
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
status: 'review',
link: null,
action: {
onClick: this.submitForReview,
title: 'Submit for review',
disabled: () =>
this.nags.filter((x) => x.condition && x.status === 'required').length > 0,
},
},
{
hide: !this.tags.rejectedStatuses.includes(this.project.status),
condition: true,
title: 'Resubmit for review',
id: 'resubmit-for-review',
description: `Your project has been ${this.project.status} by
},
toggleCollapsed: {
type: Function,
default() {
return () => {
addNotification({
group: 'main',
title: 'An error occurred',
text: 'toggleCollapsed function not found',
type: 'error',
})
}
},
},
updateMembers: {
type: Function,
default() {
return () => {
addNotification({
group: 'main',
title: 'An error occurred',
text: 'updateMembers function not found',
type: 'error',
})
}
},
},
})
const featuredGalleryImage = computed(() => props.project.gallery.find((img) => img.featured))
const nags = computed(() => [
{
condition: props.versions.length < 1,
title: 'Upload a version',
id: 'upload-version',
description: 'At least one version is required for a project to be submitted for review.',
status: 'required',
link: {
path: 'versions',
title: 'Visit versions page',
hide: props.routeName === 'type-id-versions',
},
},
{
condition:
props.project.body === '' || props.project.body.startsWith('# Placeholder description'),
title: 'Add a description',
id: 'add-description',
description:
"A description that clearly describes the project's purpose and function is required.",
status: 'required',
link: {
path: 'settings/description',
title: 'Visit description settings',
hide: props.routeName === 'type-id-settings-description',
},
},
{
condition: !props.project.icon_url,
title: 'Add an icon',
id: 'add-icon',
description:
'Your project should have a nice-looking icon to uniquely identify your project at a glance.',
status: 'suggestion',
link: {
path: 'settings',
title: 'Visit general settings',
hide: props.routeName === 'type-id-settings',
},
},
{
condition: props.project.gallery.length === 0 || !featuredGalleryImage,
title: 'Feature a gallery image',
id: 'feature-gallery-image',
description: 'Featured gallery images may be the first impression of many users.',
status: 'suggestion',
link: {
path: 'gallery',
title: 'Visit gallery page',
hide: props.routeName === 'type-id-gallery',
},
},
{
hide: props.project.versions.length === 0,
condition: props.project.categories.length < 1,
title: 'Select tags',
id: 'select-tags',
description: 'Select all tags that apply to your project.',
status: 'suggestion',
link: {
path: 'settings/tags',
title: 'Visit tag settings',
hide: props.routeName === 'type-id-settings-tags',
},
},
{
condition: !(
props.project.issues_url ||
props.project.source_url ||
props.project.wiki_url ||
props.project.discord_url ||
props.project.donation_urls.length > 0
),
title: 'Add external links',
id: 'add-links',
description:
'Add any relevant links targeted outside of Modrinth, such as sources, issues, or a Discord invite.',
status: 'suggestion',
link: {
path: 'settings/links',
title: 'Visit links settings',
hide: props.routeName === 'type-id-settings-links',
},
},
{
hide:
props.project.versions.length === 0 ||
props.project.project_type === 'resourcepack' ||
props.project.project_type === 'plugin' ||
props.project.project_type === 'shader' ||
props.project.project_type === 'datapack',
condition:
props.project.client_side === 'unknown' ||
props.project.server_side === 'unknown' ||
(props.project.client_side === 'unsupported' && props.project.server_side === 'unsupported'),
title: 'Select supported environments',
id: 'select-environments',
description: `Select if the ${formatProjectType(
props.project.project_type
).toLowerCase()} functions on the client-side and/or server-side.`,
status: 'required',
link: {
path: 'settings',
title: 'Visit general settings',
hide: props.routeName === 'type-id-settings',
},
},
{
condition: props.project.license.id === 'LicenseRef-Unknown',
title: 'Select license',
id: 'select-license',
description: `Select the license your ${formatProjectType(
props.project.project_type
).toLowerCase()} is distributed under.`,
status: 'required',
link: {
path: 'settings/license',
title: 'Visit license settings',
hide: props.routeName === 'type-id-settings-license',
},
},
{
condition: props.project.status === 'draft',
title: 'Submit for review',
id: 'submit-for-review',
description:
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
status: 'review',
link: null,
action: {
onClick: submitForReview,
title: 'Submit for review',
disabled: () => nags.value.filter((x) => x.condition && x.status === 'required').length > 0,
},
},
{
condition: props.tags.rejectedStatuses.includes(props.project.status),
title: 'Resubmit for review',
id: 'resubmit-for-review',
description: `Your project has been ${props.project.status} by
Modrinth's staff. In most cases, you can resubmit for review after
addressing the staff's message.`,
status: 'review',
link: {
path: 'moderation',
title: 'Visit moderation page',
hide: this.routeName === 'type-id-moderation',
},
},
]
.filter((x) => !x.hide)
.sort((a, b) =>
this.sortByTrue(
!a.condition,
!b.condition,
this.sortByTrue(
a.status === 'required',
b.status === 'required',
this.sortByFalse(a.status === 'review', b.status === 'review')
)
)
)
},
showInvitation() {
if (this.allMembers && this.auth) {
const member = this.allMembers.find((x) => x.user.id === this.auth.user.id)
return member && !member.accepted
}
return false
},
},
methods: {
acceptInvite() {
acceptTeamInvite(this.project.team)
this.updateMembers()
},
declineInvite() {
removeSelfFromTeam(this.project.team)
this.updateMembers()
},
sortByTrue(a, b, ifEqual = 0) {
if (a === b) {
return ifEqual
} else if (a) {
return -1
} else {
return 1
}
},
sortByFalse(a, b, ifEqual = 0) {
if (a === b) {
return ifEqual
} else if (b) {
return -1
} else {
return 1
}
},
async submitForReview() {
if (
!this.acknowledgedMessage ||
this.nags.filter((x) => x.condition && x.status === 'required').length === 0
) {
await this.setProcessing()
}
status: 'review',
link: {
path: 'moderation',
title: 'Visit moderation page',
hide: props.routeName === 'type-id-moderation',
},
},
])
const showInvitation = computed(() => {
if (props.allMembers && props.auth) {
const member = props.allMembers.find((x) => x.user.id === props.auth.user.id)
return member && !member.accepted
}
return false
})
const acceptInvite = () => {
acceptTeamInvite(props.project.team)
props.updateMembers()
}
const declineInvite = () => {
removeTeamMember(props.project.team, props.auth.user.id)
props.updateMembers()
}
const submitForReview = async () => {
if (
!props.acknowledgedMessage ||
nags.value.filter((x) => x.condition && x.status === 'required').length === 0
) {
await props.setProcessing()
}
}
</script>