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

@@ -5,7 +5,12 @@
<Breadcrumbs
current-title="Settings"
:link-stack="[
{ href: `/dashboard/projects`, label: 'Projects' },
{
href: organization
? `/organization/${organization.slug}/settings/projects`
: `/dashboard/projects`,
label: 'Projects',
},
{
href: `/${project.project_type}/${project.slug ? project.slug : project.id}`,
label: project.title,
@@ -126,10 +131,13 @@
v-model:members="members"
v-model:all-members="allMembers"
v-model:dependencies="dependencies"
v-model:organization="organization"
:current-member="currentMember"
:patch-project="patchProject"
:patch-icon="patchIcon"
:update-icon="resetProject"
:reset-project="resetProject"
:reset-organization="resetOrganization"
:reset-members="resetMembers"
:route="route"
/>
</div>
@@ -478,11 +486,15 @@
v-model:members="members"
v-model:all-members="allMembers"
v-model:dependencies="dependencies"
v-model:organization="organization"
:current-member="currentMember"
:reset-project="resetProject"
:reset-organization="resetOrganization"
:reset-members="resetMembers"
:route="route"
/>
</section>
<div class="card normal-page__info">
<div class="universal-card normal-page__info">
<template
v-if="
project.issues_url ||
@@ -618,6 +630,17 @@
<hr class="card-divider" />
</template>
<h2 class="card-header">Project members</h2>
<nuxt-link
v-if="organization"
class="team-member columns button-transparent"
:to="`/organization/${organization.slug}`"
>
<Avatar :src="organization.icon_url" :alt="organization.name" size="sm" />
<div class="member-info">
<p class="name">{{ organization.name }}</p>
<p class="role"><OrganizationIcon /> Organization</p>
</div>
</nuxt-link>
<nuxt-link
v-for="member in members"
:key="member.user.id"
@@ -787,6 +810,7 @@ import { reportProject } from '~/utils/report-helpers.ts'
import Breadcrumbs from '~/components/ui/Breadcrumbs.vue'
import { userCollectProject } from '~/composables/user.js'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
import OrganizationIcon from '~/assets/images/utils/organization.svg'
const data = useNuxtApp()
const route = useRoute()
@@ -820,14 +844,23 @@ if (
})
}
let project, allMembers, dependencies, featuredVersions, versions
let project,
resetProject,
allMembers,
resetMembers,
dependencies,
featuredVersions,
versions,
organization,
resetOrganization
try {
;[
{ data: project },
{ data: allMembers },
{ data: project, refresh: resetProject },
{ data: allMembers, refresh: resetMembers },
{ data: dependencies },
{ data: featuredVersions },
{ data: versions },
{ data: organization, refresh: resetOrganization },
] = await Promise.all([
useAsyncData(`project/${route.params.id}`, () => useBaseFetch(`project/${route.params.id}`), {
transform: (project) => {
@@ -838,10 +871,6 @@ try {
project.loaders,
tags.value
)
if (process.client && history.state && history.state.overrideProjectType) {
project.project_type = history.state.overrideProjectType
}
}
return project
@@ -870,6 +899,9 @@ try {
useAsyncData(`project/${route.params.id}/version`, () =>
useBaseFetch(`project/${route.params.id}/version`)
),
useAsyncData(`project/${route.params.id}/organization`, () =>
useBaseFetch(`project/${route.params.id}/organization`, { apiVersion: 3 })
),
])
versions = shallowRef(toRaw(versions))
@@ -903,27 +935,46 @@ if (project.value.project_type !== route.params.type || route.params.id !== proj
)
}
const members = ref(allMembers.value.filter((x) => x.accepted))
const currentMember = ref(
auth.value.user ? allMembers.value.find((x) => x.user.id === auth.value.user.id) : null
)
// Members should be an array of all members, without the accepted ones, and with the user with the Owner role at the start
// The rest of the members should be sorted by role, then by name
const members = computed(() => {
const acceptedMembers = allMembers.value.filter((x) => x.accepted)
const owner = acceptedMembers.find((x) => x.role === 'Owner')
const rest = acceptedMembers.filter((x) => x.role !== 'Owner') || []
if (
!currentMember.value &&
auth.value.user &&
tags.value.staffRoles.includes(auth.value.user.role)
) {
currentMember.value = {
team_id: project.team_id,
user: auth.value.user,
role: auth.value.role,
permissions: auth.value.user.role === 'admin' ? 1023 : 12,
accepted: true,
payouts_split: 0,
avatar_url: auth.value.user.avatar_url,
name: auth.value.user.username,
rest.sort((a, b) => {
if (a.role === b.role) {
return a.user.username.localeCompare(b.user.username)
} else {
return a.role.localeCompare(b.role)
}
})
return owner ? [owner, ...rest] : rest
})
const currentMember = computed(() => {
let val = auth.value.user ? allMembers.value.find((x) => x.user.id === auth.value.user.id) : null
if (!val && organization.value && organization.value.members) {
val = organization.value.members.find((x) => x.user.id === auth.value.user.id)
}
}
if (!val && auth.value.user && tags.value.staffRoles.includes(auth.value.user.role)) {
val = {
team_id: project.team_id,
user: auth.value.user,
role: auth.value.role,
permissions: auth.value.user.role === 'admin' ? 1023 : 12,
accepted: true,
payouts_split: 0,
avatar_url: auth.value.user.avatar_url,
name: auth.value.user.username,
}
}
return val
})
versions.value = data.$computeVersions(versions.value, allMembers.value)
@@ -955,38 +1006,36 @@ const licenseIdDisplay = computed(() => {
})
const featuredGalleryImage = computed(() => project.value.gallery.find((img) => img.featured))
const projectTypeDisplay = data.$formatProjectType(
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders)
const projectTypeDisplay = computed(() =>
data.$formatProjectType(
data.$getProjectTypeForDisplay(project.value.project_type, project.value.loaders)
)
)
const title = computed(() => `${project.value.title} - Minecraft ${projectTypeDisplay.value}`)
const description = computed(
() =>
`${project.value.description} - Download the Minecraft ${projectTypeDisplay.value} ${
project.value.title
} by ${
members.value.find((x) => x.role === 'Owner')?.user?.username || 'a Creator'
} on Modrinth`
)
const title = `${project.value.title} - Minecraft ${projectTypeDisplay}`
const description = `${project.value.description} - Download the Minecraft ${projectTypeDisplay} ${
project.value.title
} by ${members.value.find((x) => x.role === 'Owner').user.username} on Modrinth`
if (!route.name.startsWith('type-id-settings')) {
useSeoMeta({
title,
description,
ogTitle: title,
ogDescription: project.value.description,
ogImage: project.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
robots:
title: () => title.value,
description: () => description.value,
ogTitle: () => title.value,
ogDescription: () => project.value.description,
ogImage: () => project.value.icon_url ?? 'https://cdn.modrinth.com/placeholder.png',
robots: () =>
project.value.status === 'approved' || project.value.status === 'archived'
? 'all'
: 'noindex',
})
}
async function resetProject() {
const newProject = await useBaseFetch(`project/${project.value.id}`)
newProject.actualProjectType = JSON.parse(JSON.stringify(newProject.project_type))
newProject.project_type = data.$getProjectTypeForUrl(newProject.project_type, newProject.loaders)
project.value = newProject
}
async function clearMessage() {
startLoading()
@@ -1041,7 +1090,7 @@ const licenseText = ref('')
async function getLicenseData() {
try {
const text = await useBaseFetch(`tag/license/${project.value.license.id}`)
licenseText.value = text.body
licenseText.value = text.body || 'License text could not be retrieved.'
} catch {
licenseText.value = 'License text could not be retrieved.'
}
@@ -1364,6 +1413,12 @@ const collapsedChecklist = ref(false)
font-size: var(--font-size-sm);
margin: 0.2rem 0;
}
.role {
display: flex;
align-items: center;
gap: 0.25rem;
}
}
}

View File

@@ -199,7 +199,8 @@
:max-size="524288000"
:accept="acceptFileTypes"
prompt="Upload an image"
class="brand-button iconified-button"
class="iconified-button brand-button"
:disabled="!isPermission(currentMember?.permissions, 1 << 2)"
@change="handleFiles"
>
<UploadIcon />
@@ -207,7 +208,14 @@
<span class="indicator">
<InfoIcon /> Click to choose an image or drag one onto this page
</span>
<DropArea :accept="acceptFileTypes" @change="handleFiles" />
<DropArea
:accept="acceptFileTypes"
:disabled="!isPermission(currentMember?.permissions, 1 << 2)"
@change="handleFiles"
/>
</div>
<div v-else class="card header-buttons">
<span class="indicator"> <InfoIcon /> You don't have permission to upload images </span>
</div>
<div class="items">
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
@@ -288,11 +296,13 @@ import {
ImageIcon,
TransferIcon,
ConfirmModal,
FileInput,
DropArea,
} from 'omorphia'
import FileInput from '~/components/ui/FileInput.vue'
import DropArea from '~/components/ui/DropArea.vue'
import Modal from '~/components/ui/Modal.vue'
import { isPermission } from '~/utils/permissions.ts'
const props = defineProps({
project: {
type: Object,
@@ -306,6 +316,11 @@ const props = defineProps({
return null
},
},
resetProject: {
type: Function,
required: true,
default: () => {},
},
})
const title = `${props.project.title} - Gallery`
@@ -430,7 +445,7 @@ export default defineNuxtComponent({
method: 'POST',
body: this.editFile,
})
await this.updateProject()
await this.resetProject()
this.$refs.modal_edit_item.hide()
} catch (err) {
@@ -468,7 +483,7 @@ export default defineNuxtComponent({
method: 'PATCH',
})
await this.updateProject()
await this.resetProject()
this.$refs.modal_edit_item.hide()
} catch (err) {
this.$notify({
@@ -495,7 +510,7 @@ export default defineNuxtComponent({
}
)
await this.updateProject()
await this.resetProject()
} catch (err) {
this.$notify({
group: 'main',
@@ -507,16 +522,6 @@ export default defineNuxtComponent({
stopLoading()
},
async updateProject() {
const project = await useBaseFetch(`project/${this.project.id}`)
project.actualProjectType = JSON.parse(JSON.stringify(project.project_type))
project.project_type = this.$getProjectTypeForUrl(project.project_type, project.loaders)
this.$emit('update:project', project)
this.resetEdit()
},
},
})
</script>

View File

@@ -1,5 +1,9 @@
<template>
<div class="markdown-body card" v-html="renderHighlightedString(project.body)" />
<div
v-if="project.body"
class="markdown-body card"
v-html="renderHighlightedString(project.body || '')"
/>
</template>
<script>

View File

@@ -104,10 +104,13 @@ const props = defineProps({
return null
},
},
resetProject: {
type: Function,
required: true,
default: () => {},
},
})
const emit = defineEmits(['update:project'])
const app = useNuxtApp()
const auth = await useAuth()
@@ -130,7 +133,7 @@ async function setStatus(status) {
})
const project = props.project
project.status = status
emit('update:project', project)
await props.resetProject()
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
} catch (err) {
app.$notify({

View File

@@ -1,6 +1,6 @@
<template>
<div>
<Card>
<div class="universal-card">
<div class="markdown-disclaimer">
<h2>Description</h2>
<span class="label__description">
@@ -29,12 +29,12 @@
Save changes
</button>
</div>
</Card>
</div>
</div>
</template>
<script>
import { MarkdownEditor, Card } from 'omorphia'
import { MarkdownEditor } from 'omorphia'
import Chips from '~/components/ui/Chips.vue'
import SaveIcon from '~/assets/images/utils/save.svg'
import { renderHighlightedString } from '~/helpers/highlight.js'
@@ -42,7 +42,6 @@ import { useImageUpload } from '~/composables/image-upload.ts'
export default defineNuxtComponent({
components: {
Card,
Chips,
SaveIcon,
MarkdownEditor,

View File

@@ -91,6 +91,7 @@
</div>
<template
v-if="
project.versions.length !== 0 &&
project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' &&
project.project_type !== 'shader' &&
@@ -110,6 +111,7 @@
<Multiselect
id="project-env-client"
v-model="clientSide"
class="small-multiselect"
placeholder="Select one"
:options="sideTypes"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
@@ -133,6 +135,7 @@
<Multiselect
id="project-env-server"
v-model="serverSide"
class="small-multiselect"
placeholder="Select one"
:options="sideTypes"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
@@ -190,6 +193,7 @@
<Multiselect
id="project-visibility"
v-model="visibility"
class="small-multiselect"
placeholder="Select one"
:options="tags.approvedStatuses"
:custom-label="(value) => $formatProjectStatus(value)"
@@ -236,8 +240,9 @@
</div>
</template>
<script>
<script setup>
import { Multiselect } from 'vue-multiselect'
import Avatar from '~/components/ui/Avatar.vue'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import FileInput from '~/components/ui/FileInput.vue'
@@ -249,198 +254,160 @@ import ExitIcon from '~/assets/images/utils/x.svg'
import IssuesIcon from '~/assets/images/utils/issues.svg'
import CheckIcon from '~/assets/images/utils/check.svg'
export default defineNuxtComponent({
components: {
Avatar,
ModalConfirm,
FileInput,
Multiselect,
UploadIcon,
SaveIcon,
TrashIcon,
ExitIcon,
CheckIcon,
IssuesIcon,
const props = defineProps({
project: {
type: Object,
required: true,
default: () => ({}),
},
props: {
project: {
type: Object,
default() {
return {}
},
},
currentMember: {
type: Object,
default() {
return null
},
},
patchProject: {
type: Function,
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'Patch project function not found',
type: 'error',
})
}
},
},
patchIcon: {
type: Function,
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'Patch icon function not found',
type: 'error',
})
}
},
},
updateIcon: {
type: Function,
default() {
return () => {
this.$notify({
group: 'main',
title: 'An error occurred',
text: 'Update icon function not found',
type: 'error',
})
}
},
},
currentMember: {
type: Object,
required: true,
default: () => ({}),
},
setup() {
const tags = useTags()
return { tags }
patchProject: {
type: Function,
required: true,
default: () => {},
},
data() {
return {
name: this.project.title,
slug: this.project.slug,
summary: this.project.description,
icon: null,
previewImage: null,
clientSide: this.project.client_side,
serverSide: this.project.server_side,
deletedIcon: false,
visibility: this.tags.approvedStatuses.includes(this.project.status)
? this.project.status
: this.project.requested_status,
}
patchIcon: {
type: Function,
required: true,
default: () => {},
},
computed: {
hasPermission() {
const EDIT_DETAILS = 1 << 2
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
},
hasDeletePermission() {
const DELETE_PROJECT = 1 << 7
return (this.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
},
sideTypes() {
return ['required', 'optional', 'unsupported']
},
patchData() {
const data = {}
if (this.name !== this.project.title) {
data.title = this.name.trim()
}
if (this.slug !== this.project.slug) {
data.slug = this.slug.trim()
}
if (this.summary !== this.project.description) {
data.description = this.summary.trim()
}
if (this.clientSide !== this.project.client_side) {
data.client_side = this.clientSide
}
if (this.serverSide !== this.project.server_side) {
data.server_side = this.serverSide
}
if (this.tags.approvedStatuses.includes(this.project.status)) {
if (this.visibility !== this.project.status) {
data.status = this.visibility
}
} else if (this.visibility !== this.project.requested_status) {
data.requested_status = this.visibility
}
return data
},
hasChanges() {
return Object.keys(this.patchData).length > 0 || this.deletedIcon || this.icon
},
},
methods: {
hasModifiedVisibility() {
const originalVisibility = this.tags.approvedStatuses.includes(this.project.status)
? this.project.status
: this.project.requested_status
return originalVisibility !== this.visibility
},
async saveChanges() {
if (this.hasChanges) {
await this.patchProject(this.patchData)
}
if (this.deletedIcon) {
await this.deleteIcon()
this.deletedIcon = false
} else if (this.icon) {
await this.patchIcon(this.icon)
this.icon = null
}
},
showPreviewImage(files) {
const reader = new FileReader()
this.icon = files[0]
this.deletedIcon = false
reader.readAsDataURL(this.icon)
reader.onload = (event) => {
this.previewImage = event.target.result
}
},
async deleteProject() {
await useBaseFetch(`project/${this.project.id}`, {
method: 'DELETE',
})
await initUserProjects()
await this.$router.push('/dashboard/projects')
this.$notify({
group: 'main',
title: 'Project deleted',
text: 'Your project has been deleted.',
type: 'success',
})
},
markIconForDeletion() {
this.deletedIcon = true
this.icon = null
this.previewImage = null
},
async deleteIcon() {
await useBaseFetch(`project/${this.project.id}/icon`, {
method: 'DELETE',
})
await this.updateIcon()
this.$notify({
group: 'main',
title: 'Project icon removed',
text: "Your project's icon has been removed.",
type: 'success',
})
},
resetProject: {
type: Function,
required: true,
default: () => {},
},
})
const tags = useTags()
const router = useRouter()
const name = ref(props.project.title)
const slug = ref(props.project.slug)
const summary = ref(props.project.description)
const icon = ref(null)
const previewImage = ref(null)
const clientSide = ref(props.project.client_side)
const serverSide = ref(props.project.server_side)
const deletedIcon = ref(false)
const visibility = ref(
tags.value.approvedStatuses.includes(props.project.status)
? props.project.status
: props.project.requested_status
)
const hasPermission = computed(() => {
const EDIT_DETAILS = 1 << 2
return (props.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS
})
const hasDeletePermission = computed(() => {
const DELETE_PROJECT = 1 << 7
return (props.currentMember.permissions & DELETE_PROJECT) === DELETE_PROJECT
})
const sideTypes = ['required', 'optional', 'unsupported']
const patchData = computed(() => {
const data = {}
if (name.value !== props.project.title) {
data.title = name.value.trim()
}
if (slug.value !== props.project.slug) {
data.slug = slug.value.trim()
}
if (summary.value !== props.project.description) {
data.description = summary.value.trim()
}
if (clientSide.value !== props.project.client_side) {
data.client_side = clientSide.value
}
if (serverSide.value !== props.project.server_side) {
data.server_side = serverSide.value
}
if (tags.value.approvedStatuses.includes(props.project.status)) {
if (visibility.value !== props.project.status) {
data.status = visibility.value
}
} else if (visibility.value !== props.project.requested_status) {
data.requested_status = visibility.value
}
return data
})
const hasChanges = computed(() => {
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value
})
const hasModifiedVisibility = () => {
const originalVisibility = tags.value.approvedStatuses.includes(props.project.status)
? props.project.status
: props.project.requested_status
return originalVisibility !== visibility.value
}
const saveChanges = async () => {
if (hasChanges.value) {
await props.patchProject(patchData.value)
}
if (deletedIcon.value) {
await deleteIcon()
deletedIcon.value = false
} else if (icon.value) {
await props.patchIcon(icon.value)
icon.value = null
}
}
const showPreviewImage = (files) => {
const reader = new FileReader()
icon.value = files[0]
deletedIcon.value = false
reader.readAsDataURL(icon.value)
reader.onload = (event) => {
previewImage.value = event.target.result
}
}
const deleteProject = async () => {
await useBaseFetch(`project/${props.project.id}`, {
method: 'DELETE',
})
await initUserProjects()
await router.push('/dashboard/projects')
addNotification({
group: 'main',
title: 'Project deleted',
text: 'Your project has been deleted.',
type: 'success',
})
}
const markIconForDeletion = () => {
deletedIcon.value = true
icon.value = null
previewImage.value = null
}
const deleteIcon = async () => {
await useBaseFetch(`project/${props.project.id}/icon`, {
method: 'DELETE',
})
await props.resetProject()
addNotification({
group: 'main',
title: 'Project icon removed',
text: "Your project's icon has been removed.",
type: 'success',
})
}
</script>
<style lang="scss" scoped>
.visibility-info {
@@ -467,7 +434,7 @@ svg {
max-width: 24rem;
}
.multiselect {
.small-multiselect {
max-width: 15rem;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -11,44 +11,82 @@
{{ $formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
that apply.
</p>
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
<p v-if="project.versions.length === 0" class="known-errors">
Please upload a version first in order to select tags!
</p>
<template v-else>
<template v-for="header in Object.keys(categoryLists)" :key="`categories-${header}`">
<div class="label">
<h4>
<span class="label__title">{{ $formatCategoryHeader(header) }}</span>
</h4>
<span class="label__description">
<template v-if="header === 'categories'">
Select all categories that reflect the themes or function of your
{{ $formatProjectType(project.project_type).toLowerCase() }}.
</template>
<template v-else-if="header === 'features'">
Select all of the features that your
{{ $formatProjectType(project.project_type).toLowerCase() }} makes use of.
</template>
<template v-else-if="header === 'resolutions'">
Select the resolution(s) of textures in your
{{ $formatProjectType(project.project_type).toLowerCase() }}.
</template>
<template v-else-if="header === 'performance impact'">
Select the realistic performance impact of your
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to
different levels of performance impact.
</template>
</span>
</div>
<div class="category-list input-div">
<Checkbox
v-for="category in categoryLists[header]"
:key="`category-${header}-${category.name}`"
:model-value="selectedTags.includes(category)"
:description="$formatCategory(category.name)"
class="category-selector"
@update:model-value="toggleCategory(category)"
>
<div class="category-selector__label">
<div
v-if="header !== 'resolutions' && category.icon"
aria-hidden="true"
class="icon"
v-html="category.icon"
/>
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
</div>
</Checkbox>
</div>
</template>
<div class="label">
<h4>
<span class="label__title">{{ $formatCategoryHeader(header) }}</span>
<span class="label__title"><StarIcon /> Featured tags</span>
</h4>
<span class="label__description">
<template v-if="header === 'categories'">
Select all categories that reflect the themes or function of your
{{ $formatProjectType(project.project_type).toLowerCase() }}.
</template>
<template v-else-if="header === 'features'">
Select all of the features that your
{{ $formatProjectType(project.project_type).toLowerCase() }} makes use of.
</template>
<template v-else-if="header === 'resolutions'">
Select the resolution(s) of textures in your
{{ $formatProjectType(project.project_type).toLowerCase() }}.
</template>
<template v-else-if="header === 'performance impact'">
Select the realistic performance impact of your
{{ $formatProjectType(project.project_type).toLowerCase() }}. Select multiple if the
{{ $formatProjectType(project.project_type).toLowerCase() }} is configurable to
different levels of performance impact.
</template>
You can feature up to 3 of your most relevant tags. Other tags may be promoted to
featured if you do not select all 3.
</span>
</div>
<p v-if="selectedTags.length < 1">
Select at least one category in order to feature a category.
</p>
<div class="category-list input-div">
<Checkbox
v-for="category in categoryLists[header]"
:key="`category-${header}-${category.name}`"
:model-value="selectedTags.includes(category)"
:description="$formatCategory(category.name)"
v-for="category in selectedTags"
:key="`featured-category-${category.name}`"
class="category-selector"
@update:model-value="toggleCategory(category)"
:model-value="featuredTags.includes(category)"
:description="$formatCategory(category.name)"
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
@update:model-value="toggleFeaturedCategory(category)"
>
<div class="category-selector__label">
<div
v-if="header !== 'resolutions' && category.icon"
v-if="category.header !== 'resolutions' && category.icon"
aria-hidden="true"
class="icon"
v-html="category.icon"
@@ -58,39 +96,7 @@
</Checkbox>
</div>
</template>
<div class="label">
<h4>
<span class="label__title"><StarIcon /> Featured tags</span>
</h4>
<span class="label__description">
You can feature up to 3 of your most relevant tags. Other tags may be promoted to featured
if you do not select all 3.
</span>
</div>
<p v-if="selectedTags.length < 1">
Select at least one category in order to feature a category.
</p>
<div class="category-list input-div">
<Checkbox
v-for="category in selectedTags"
:key="`featured-category-${category.name}`"
class="category-selector"
:model-value="featuredTags.includes(category)"
:description="$formatCategory(category.name)"
:disabled="featuredTags.length >= 3 && !featuredTags.includes(category)"
@update:model-value="toggleFeaturedCategory(category)"
>
<div class="category-selector__label">
<div
v-if="category.header !== 'resolutions' && category.icon"
aria-hidden="true"
class="icon"
v-html="category.icon"
/>
<span aria-hidden="true"> {{ $formatCategory(category.name) }}</span>
</div>
</Checkbox>
</div>
<div class="button-group">
<button
type="button"

View File

@@ -541,7 +541,7 @@
:custom-label="(value) => $formatCategory(value)"
:loading="tags.loaders.length === 0"
:multiple="true"
:searchable="false"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
@@ -735,6 +735,11 @@ export default defineNuxtComponent({
return {}
},
},
resetProject: {
type: Function,
required: true,
default: () => {},
},
},
async setup(props) {
const data = useNuxtApp()
@@ -1135,13 +1140,13 @@ export default defineNuxtComponent({
})
}
const newEditedVersions = await this.resetProjectVersions()
await this.resetProjectVersions()
await this.$router.replace(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.id
}/version/${encodeURI(
newEditedVersions.find((x) => x.id === this.version.id).displayUrlEnding
this.versions.find((x) => x.id === this.version.id).displayUrlEnding
)}`
)
} catch (err) {
@@ -1316,6 +1321,7 @@ export default defineNuxtComponent({
useBaseFetch(`project/${this.version.project_id}/version`),
useBaseFetch(`project/${this.version.project_id}/version?featured=true`),
useBaseFetch(`project/${this.version.project_id}/dependencies`),
this.resetProject(),
])
const newCreatedVersions = this.$computeVersions(versions, this.members)

View File

@@ -5,7 +5,8 @@
:max-size="524288000"
:accept="acceptFileFromProjectType(project.project_type)"
prompt="Upload a version"
class="brand-button iconified-button"
class="iconified-button brand-button"
:disabled="!isPermission(currentMember?.permissions, 1 << 0)"
@change="handleFiles"
>
<UploadIcon />