You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user