You've already forked AstralRinth
forked from didirus/AstralRinth
feat: ssr fixes + switch project page to tanstack (#5192)
* feat: ssr fixes * feat: lazy load non-core data * feat: ssr timing debugging * feat: go back to all parallel * feat: migrate to DI + set up mutators * feat: remove double get versions request, only call v3 * refactor: [version].vue page to use composition API and typescript * feat: gallery.vue start * fix: remove left behind console log * fix: type issues + gallery * fix: versionsummary modal + version page direct join * fix: projectRaw guard * fix: currentMember val fix * fix: actualProjectType * fix: vers summary link same page * fix: lint --------- Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
@@ -11,8 +11,8 @@
|
|||||||
"lint": "eslint . && prettier --check .",
|
"lint": "eslint . && prettier --check .",
|
||||||
"fix": "eslint . --fix && prettier --write .",
|
"fix": "eslint . --fix && prettier --write .",
|
||||||
"intl:extract": "formatjs extract \"src/{components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
"intl:extract": "formatjs extract \"src/{components,composables,layouts,middleware,modules,pages,plugins,utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||||
"cf-deploy": "pnpm run build && wrangler deploy --env preview",
|
"cf-deploy": "pnpm run build && wrangler deploy --env staging",
|
||||||
"cf-dev": "pnpm run build && wrangler dev --env preview",
|
"cf-dev": "pnpm run build && wrangler dev --env staging",
|
||||||
"cf-typegen": "wrangler types"
|
"cf-typegen": "wrangler types"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -2,78 +2,54 @@
|
|||||||
<nav
|
<nav
|
||||||
ref="scrollContainer"
|
ref="scrollContainer"
|
||||||
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
|
||||||
:class="[mode === 'navigation' ? 'card-shadow' : undefined]"
|
:class="{ 'card-shadow': mode === 'navigation' }"
|
||||||
>
|
>
|
||||||
<template v-if="mode === 'navigation'">
|
<template v-if="mode === 'navigation'">
|
||||||
<NuxtLink
|
<NuxtLink
|
||||||
v-for="(link, index) in filteredLinks"
|
v-for="(link, index) in filteredLinks"
|
||||||
v-show="link.shown === undefined ? true : link.shown"
|
v-show="link.shown ?? true"
|
||||||
:key="link.href"
|
:key="link.href"
|
||||||
ref="tabLinkElements"
|
ref="tabLinkElements"
|
||||||
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
|
||||||
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
|
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
|
||||||
|
:class="getSSRFallbackClasses(index)"
|
||||||
|
@mouseenter="link.onHover?.()"
|
||||||
|
@focus="link.onHover?.()"
|
||||||
>
|
>
|
||||||
<component
|
<component :is="link.icon" v-if="link.icon" class="size-5" :class="getIconClasses(index)" />
|
||||||
:is="link.icon"
|
<span class="text-nowrap" :class="getLabelClasses(index)">
|
||||||
v-if="link.icon"
|
{{ link.label }}
|
||||||
class="size-5"
|
</span>
|
||||||
:class="{
|
|
||||||
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
|
|
||||||
'text-secondary': currentActiveIndex !== index || subpageSelected,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="text-nowrap"
|
|
||||||
:class="{
|
|
||||||
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
|
|
||||||
'text-contrast': currentActiveIndex !== index || subpageSelected,
|
|
||||||
}"
|
|
||||||
>{{ link.label }}</span
|
|
||||||
>
|
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<div
|
||||||
v-for="(link, index) in filteredLinks"
|
v-for="(link, index) in filteredLinks"
|
||||||
v-show="link.shown === undefined ? true : link.shown"
|
v-show="link.shown ?? true"
|
||||||
:key="link.href"
|
:key="link.href"
|
||||||
ref="tabLinkElements"
|
ref="tabLinkElements"
|
||||||
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 hover:cursor-pointer focus:rounded-full"
|
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 hover:cursor-pointer focus:rounded-full"
|
||||||
|
:class="getSSRFallbackClasses(index)"
|
||||||
@click="emit('tabClick', index, link)"
|
@click="emit('tabClick', index, link)"
|
||||||
>
|
>
|
||||||
<component
|
<component :is="link.icon" v-if="link.icon" class="size-5" :class="getIconClasses(index)" />
|
||||||
:is="link.icon"
|
<span class="text-nowrap" :class="getLabelClasses(index)">
|
||||||
v-if="link.icon"
|
{{ link.label }}
|
||||||
class="size-5"
|
</span>
|
||||||
:class="{
|
|
||||||
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
|
|
||||||
'text-secondary': currentActiveIndex !== index || subpageSelected,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="text-nowrap"
|
|
||||||
:class="{
|
|
||||||
'text-button-textSelected': currentActiveIndex === index && !subpageSelected,
|
|
||||||
'text-contrast': currentActiveIndex !== index || subpageSelected,
|
|
||||||
}"
|
|
||||||
>{{ link.label }}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Animated slider background -->
|
||||||
<div
|
<div
|
||||||
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
|
class="pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1"
|
||||||
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
|
:class="[
|
||||||
}`"
|
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected',
|
||||||
:style="{
|
{ 'navtabs-transition': transitionsEnabled },
|
||||||
left: sliderLeftPx,
|
]"
|
||||||
top: sliderTopPx,
|
:style="sliderStyle"
|
||||||
right: sliderRightPx,
|
|
||||||
bottom: sliderBottomPx,
|
|
||||||
opacity:
|
|
||||||
sliderLeft === 4 && sliderLeft === sliderRight ? 0 : currentActiveIndex === -1 ? 0 : 1,
|
|
||||||
}"
|
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></div>
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -89,6 +65,7 @@ interface Tab {
|
|||||||
shown?: boolean
|
shown?: boolean
|
||||||
icon?: Component
|
icon?: Component
|
||||||
subpages?: string[]
|
subpages?: string[]
|
||||||
|
onHover?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -109,124 +86,194 @@ const emit = defineEmits<{
|
|||||||
tabClick: [index: number, tab: Tab]
|
tabClick: [index: number, tab: Tab]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// DOM refs
|
||||||
const scrollContainer = ref<HTMLElement | null>(null)
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
const tabLinkElements = ref<HTMLElement[]>()
|
||||||
|
|
||||||
|
// Slider pos state
|
||||||
const sliderLeft = ref(4)
|
const sliderLeft = ref(4)
|
||||||
const sliderTop = ref(4)
|
const sliderTop = ref(4)
|
||||||
const sliderRight = ref(4)
|
const sliderRight = ref(4)
|
||||||
const sliderBottom = ref(4)
|
const sliderBottom = ref(4)
|
||||||
|
|
||||||
|
// active tab state
|
||||||
const currentActiveIndex = ref(-1)
|
const currentActiveIndex = ref(-1)
|
||||||
const subpageSelected = ref(false)
|
const subpageSelected = ref(false)
|
||||||
|
|
||||||
const filteredLinks = computed(() =>
|
// SSR state
|
||||||
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
|
const sliderReady = ref(false) // Slider is positioned and should be visible
|
||||||
|
const transitionsEnabled = ref(false) // CSS transitions should apply (after first paint)
|
||||||
|
|
||||||
|
const filteredLinks = computed(() => props.links.filter((link) => link.shown ?? true))
|
||||||
|
|
||||||
|
const sliderStyle = computed(() => ({
|
||||||
|
left: `${sliderLeft.value}px`,
|
||||||
|
top: `${sliderTop.value}px`,
|
||||||
|
right: `${sliderRight.value}px`,
|
||||||
|
bottom: `${sliderBottom.value}px`,
|
||||||
|
opacity: sliderReady.value && currentActiveIndex.value !== -1 ? 1 : 0,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isActiveAndNotSubpage = computed(
|
||||||
|
() => (index: number) => currentActiveIndex.value === index && !subpageSelected.value,
|
||||||
)
|
)
|
||||||
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
|
|
||||||
const sliderTopPx = computed(() => `${sliderTop.value}px`)
|
|
||||||
const sliderRightPx = computed(() => `${sliderRight.value}px`)
|
|
||||||
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
|
|
||||||
|
|
||||||
const tabLinkElements = ref()
|
function getSSRFallbackClasses(index: number) {
|
||||||
|
if (sliderReady.value) return {}
|
||||||
|
if (currentActiveIndex.value !== index) return {}
|
||||||
|
|
||||||
function pickLink() {
|
return {
|
||||||
let index = -1
|
'rounded-full': true,
|
||||||
subpageSelected.value = false
|
'bg-button-bgSelected': !subpageSelected.value,
|
||||||
|
'bg-button-bg': subpageSelected.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIconClasses(index: number) {
|
||||||
|
return {
|
||||||
|
'text-button-textSelected': isActiveAndNotSubpage.value(index),
|
||||||
|
'text-secondary': !isActiveAndNotSubpage.value(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelClasses(index: number) {
|
||||||
|
return {
|
||||||
|
'text-button-textSelected': isActiveAndNotSubpage.value(index),
|
||||||
|
'text-contrast': !isActiveAndNotSubpage.value(index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeActiveIndex(): { index: number; isSubpage: boolean } {
|
||||||
if (props.mode === 'local' && props.activeIndex !== undefined) {
|
if (props.mode === 'local' && props.activeIndex !== undefined) {
|
||||||
index = Math.min(props.activeIndex, filteredLinks.value.length - 1)
|
return {
|
||||||
} else {
|
index: Math.min(props.activeIndex, filteredLinks.value.length - 1),
|
||||||
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
isSubpage: false,
|
||||||
const link = filteredLinks.value[i]
|
|
||||||
if (props.query) {
|
|
||||||
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
} else if (decodeURIComponent(route.path) === link.href) {
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
} else if (
|
|
||||||
decodeURIComponent(route.path).includes(link.href) ||
|
|
||||||
(link.subpages &&
|
|
||||||
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
|
|
||||||
) {
|
|
||||||
index = i
|
|
||||||
subpageSelected.value = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentActiveIndex.value = index
|
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
|
||||||
|
const link = filteredLinks.value[i]
|
||||||
|
const decodedPath = decodeURIComponent(route.path)
|
||||||
|
|
||||||
if (currentActiveIndex.value !== -1) {
|
// Query-based matching
|
||||||
nextTick(() => startAnimation())
|
if (props.query) {
|
||||||
|
const queryValue = route.query[props.query]
|
||||||
|
if (queryValue === link.href || (!queryValue && !link.href)) {
|
||||||
|
return { index: i, isSubpage: false }
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exact path match
|
||||||
|
if (decodedPath === link.href) {
|
||||||
|
return { index: i, isSubpage: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subpage match
|
||||||
|
const isSubpageMatch =
|
||||||
|
decodedPath.includes(link.href) ||
|
||||||
|
link.subpages?.some((subpage) => decodedPath.includes(subpage))
|
||||||
|
|
||||||
|
if (isSubpageMatch) {
|
||||||
|
return { index: i, isSubpage: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { index: -1, isSubpage: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTabElement(index: number): HTMLElement | null {
|
||||||
|
if (!tabLinkElements.value?.[index]) return null
|
||||||
|
|
||||||
|
// In navigation mode, elements are NuxtLinks with $el property
|
||||||
|
// In local mode, elements are plain divs
|
||||||
|
const element = tabLinkElements.value[index]
|
||||||
|
return props.mode === 'navigation' ? (element as any).$el : element
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionSlider() {
|
||||||
|
const el = getTabElement(currentActiveIndex.value)
|
||||||
|
if (!el?.offsetParent) return
|
||||||
|
|
||||||
|
const parent = el.offsetParent as HTMLElement
|
||||||
|
const newPosition = {
|
||||||
|
left: el.offsetLeft,
|
||||||
|
top: el.offsetTop,
|
||||||
|
right: parent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
||||||
|
bottom: parent.offsetHeight - el.offsetTop - el.offsetHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInitialPosition = sliderLeft.value === 4 && sliderRight.value === 4
|
||||||
|
|
||||||
|
if (isInitialPosition) {
|
||||||
|
// Initial positioning: set position instantly, no animation
|
||||||
|
sliderLeft.value = newPosition.left
|
||||||
|
sliderRight.value = newPosition.right
|
||||||
|
sliderTop.value = newPosition.top
|
||||||
|
sliderBottom.value = newPosition.bottom
|
||||||
|
|
||||||
|
sliderReady.value = true
|
||||||
|
|
||||||
|
// enable transitions after slider is painted, so future changes animate
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
transitionsEnabled.value = true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
animateSliderTo(newPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function animateSliderTo(newPosition: {
|
||||||
|
left: number
|
||||||
|
top: number
|
||||||
|
right: number
|
||||||
|
bottom: number
|
||||||
|
}) {
|
||||||
|
const STAGGER_DELAY = 200
|
||||||
|
|
||||||
|
// Horizontal animation - lead with the direction of movement
|
||||||
|
if (newPosition.left < sliderLeft.value) {
|
||||||
|
sliderLeft.value = newPosition.left
|
||||||
|
setTimeout(() => (sliderRight.value = newPosition.right), STAGGER_DELAY)
|
||||||
|
} else {
|
||||||
|
sliderRight.value = newPosition.right
|
||||||
|
setTimeout(() => (sliderLeft.value = newPosition.left), STAGGER_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical animation - lead with the direction of movement
|
||||||
|
if (newPosition.top < sliderTop.value) {
|
||||||
|
sliderTop.value = newPosition.top
|
||||||
|
setTimeout(() => (sliderBottom.value = newPosition.bottom), STAGGER_DELAY)
|
||||||
|
} else {
|
||||||
|
sliderBottom.value = newPosition.bottom
|
||||||
|
setTimeout(() => (sliderTop.value = newPosition.top), STAGGER_DELAY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActiveTab() {
|
||||||
|
const { index, isSubpage } = computeActiveIndex()
|
||||||
|
currentActiveIndex.value = index
|
||||||
|
subpageSelected.value = isSubpage
|
||||||
|
|
||||||
|
if (index !== -1) {
|
||||||
|
nextTick(positionSlider)
|
||||||
} else {
|
} else {
|
||||||
sliderLeft.value = 0
|
sliderLeft.value = 0
|
||||||
sliderRight.value = 0
|
sliderRight.value = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAnimation() {
|
const initialActive = computeActiveIndex()
|
||||||
// In navigation mode, elements are NuxtLinks with $el property
|
currentActiveIndex.value = initialActive.index
|
||||||
// In local mode, elements are plain divs
|
subpageSelected.value = initialActive.isSubpage
|
||||||
const el =
|
|
||||||
props.mode === 'navigation'
|
|
||||||
? tabLinkElements.value[currentActiveIndex.value]?.$el
|
|
||||||
: tabLinkElements.value[currentActiveIndex.value]
|
|
||||||
|
|
||||||
if (!el || !el.offsetParent) return
|
onMounted(updateActiveTab)
|
||||||
|
|
||||||
const newValues = {
|
|
||||||
left: el.offsetLeft,
|
|
||||||
top: el.offsetTop,
|
|
||||||
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
|
|
||||||
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sliderLeft.value === 4 && sliderRight.value === 4) {
|
|
||||||
sliderLeft.value = newValues.left
|
|
||||||
sliderRight.value = newValues.right
|
|
||||||
sliderTop.value = newValues.top
|
|
||||||
sliderBottom.value = newValues.bottom
|
|
||||||
} else {
|
|
||||||
const delay = 200
|
|
||||||
|
|
||||||
if (newValues.left < sliderLeft.value) {
|
|
||||||
sliderLeft.value = newValues.left
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderRight.value = newValues.right
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
sliderRight.value = newValues.right
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderLeft.value = newValues.left
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newValues.top < sliderTop.value) {
|
|
||||||
sliderTop.value = newValues.top
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderBottom.value = newValues.bottom
|
|
||||||
}, delay)
|
|
||||||
} else {
|
|
||||||
sliderBottom.value = newValues.bottom
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderTop.value = newValues.top
|
|
||||||
}, delay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
pickLink()
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [route.path, route.query],
|
() => [route.path, route.query],
|
||||||
() => {
|
() => {
|
||||||
if (props.mode === 'navigation') {
|
if (props.mode === 'navigation') {
|
||||||
pickLink()
|
updateActiveTab()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -235,19 +282,12 @@ watch(
|
|||||||
() => props.activeIndex,
|
() => props.activeIndex,
|
||||||
() => {
|
() => {
|
||||||
if (props.mode === 'local') {
|
if (props.mode === 'local') {
|
||||||
pickLink()
|
updateActiveTab()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(() => props.links, updateActiveTab, { deep: true })
|
||||||
() => props.links,
|
|
||||||
() => {
|
|
||||||
// Re-trigger animation when links change
|
|
||||||
pickLink()
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import {
|
import {
|
||||||
AsteriskIcon,
|
AsteriskIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
@@ -90,7 +91,6 @@ import {
|
|||||||
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
|
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
|
||||||
import { nags } from '@modrinth/moderation'
|
import { nags } from '@modrinth/moderation'
|
||||||
import { ButtonStyled, defineMessages, type MessageDescriptor, useVIntl } from '@modrinth/ui'
|
import { ButtonStyled, defineMessages, type MessageDescriptor, useVIntl } from '@modrinth/ui'
|
||||||
import type { Project, User, Version } from '@modrinth/utils'
|
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
@@ -98,16 +98,10 @@ interface Tags {
|
|||||||
rejectedStatuses: string[]
|
rejectedStatuses: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Member {
|
|
||||||
accepted?: boolean
|
|
||||||
project_role?: string
|
|
||||||
user?: Partial<User>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
project: Project
|
project: Labrinth.Projects.v2.Project
|
||||||
versions?: Version[]
|
versions?: Labrinth.Versions.v2.Version[]
|
||||||
currentMember?: Member | null
|
currentMember?: Labrinth.Projects.v3.TeamMember | null
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
routeName?: string
|
routeName?: string
|
||||||
tags: Tags
|
tags: Tags
|
||||||
@@ -179,7 +173,7 @@ const emit = defineEmits<{
|
|||||||
const nagContext = computed<NagContext>(() => ({
|
const nagContext = computed<NagContext>(() => ({
|
||||||
project: props.project,
|
project: props.project,
|
||||||
versions: props.versions,
|
versions: props.versions,
|
||||||
currentMember: props.currentMember as User,
|
currentMember: props.currentMember?.user as Labrinth.Users.v2.User,
|
||||||
currentRoute: props.routeName,
|
currentRoute: props.routeName,
|
||||||
tags: props.tags,
|
tags: props.tags,
|
||||||
submitProject: submitForReview,
|
submitProject: submitForReview,
|
||||||
|
|||||||
52
apps/frontend/src/composables/queries/project.ts
Normal file
52
apps/frontend/src/composables/queries/project.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { AbstractModrinthClient } from '@modrinth/api-client'
|
||||||
|
|
||||||
|
const STALE_TIME = 1000 * 60 * 5 // 5 minutes
|
||||||
|
|
||||||
|
export const projectQueryOptions = {
|
||||||
|
v2: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', 'v2', projectId] as const,
|
||||||
|
queryFn: () => client.labrinth.projects_v2.get(projectId),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
|
||||||
|
v3: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', 'v3', projectId] as const,
|
||||||
|
queryFn: () => client.labrinth.projects_v3.get(projectId),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
|
||||||
|
members: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', projectId, 'members'] as const,
|
||||||
|
queryFn: () => client.labrinth.projects_v3.getMembers(projectId),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
|
||||||
|
dependencies: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', projectId, 'dependencies'] as const,
|
||||||
|
queryFn: () => client.labrinth.projects_v2.getDependencies(projectId),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
|
||||||
|
versionsV2: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', projectId, 'versions', 'v2'] as const,
|
||||||
|
queryFn: () =>
|
||||||
|
client.labrinth.versions_v3.getProjectVersions(projectId, { include_changelog: false }),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
|
||||||
|
versionsV3: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', projectId, 'versions', 'v3'] as const,
|
||||||
|
queryFn: () =>
|
||||||
|
client.labrinth.versions_v3.getProjectVersions(projectId, {
|
||||||
|
include_changelog: false,
|
||||||
|
apiVersion: 3,
|
||||||
|
}),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
|
||||||
|
organization: (projectId: string, client: AbstractModrinthClient) => ({
|
||||||
|
queryKey: ['project', projectId, 'organization'] as const,
|
||||||
|
queryFn: () => client.labrinth.projects_v3.getOrganization(projectId),
|
||||||
|
staleTime: STALE_TIME,
|
||||||
|
}),
|
||||||
|
}
|
||||||
14
apps/frontend/src/composables/query-client.ts
Normal file
14
apps/frontend/src/composables/query-client.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { QueryClient } from '@tanstack/vue-query'
|
||||||
|
import { useQueryClient } from '@tanstack/vue-query'
|
||||||
|
import { getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
|
export function useAppQueryClient(): QueryClient {
|
||||||
|
// In components, use the standard composable
|
||||||
|
if (getCurrentInstance()) {
|
||||||
|
return useQueryClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
// In middleware/server context, use the provided instance
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
return nuxtApp.$queryClient as QueryClient
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useGeneratedState } from '~/composables/generated'
|
||||||
|
import { useAppQueryClient } from '~/composables/query-client'
|
||||||
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
|
import { getProjectTypeForUrlShorthand } from '~/helpers/projects.js'
|
||||||
import { useServerModrinthClient } from '~/server/utils/api-client'
|
import { useServerModrinthClient } from '~/server/utils/api-client'
|
||||||
|
|
||||||
@@ -10,15 +12,30 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryClient = useAppQueryClient()
|
||||||
const authToken = useCookie('auth-token')
|
const authToken = useCookie('auth-token')
|
||||||
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
|
const client = useServerModrinthClient({ authToken: authToken.value || undefined })
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
|
const projectId = to.params.id as string
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const project = await client.labrinth.projects_v2.get(to.params.id as string)
|
// Fetch v2 project for redirect check AND cache it for the page
|
||||||
|
// Using fetchQuery ensures the page's useQuery gets this cached result
|
||||||
|
const project = await queryClient.fetchQuery({
|
||||||
|
queryKey: ['project', 'v2', projectId],
|
||||||
|
queryFn: () => client.labrinth.projects_v2.get(projectId),
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
})
|
||||||
|
|
||||||
if (!project) {
|
// Let page handle 404
|
||||||
return
|
if (!project) return
|
||||||
|
|
||||||
|
// Cache by slug if we looked up by ID (or vice versa)
|
||||||
|
if (projectId !== project.slug) {
|
||||||
|
queryClient.setQueryData(['project', 'v2', project.slug], project)
|
||||||
|
}
|
||||||
|
if (projectId !== project.id) {
|
||||||
|
queryClient.setQueryData(['project', 'v2', project.id], project)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine the correct URL type
|
// Determine the correct URL type
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,108 +1,108 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="mb-3 flex">
|
<!-- Loading state for initial version load -->
|
||||||
<VersionFilterControl
|
<div
|
||||||
:versions="props.versions"
|
v-if="versionsLoading && !versions?.length"
|
||||||
:game-versions="tags.gameVersions"
|
class="flex items-center justify-center gap-2 py-8"
|
||||||
@update:query="updateQuery"
|
>
|
||||||
/>
|
<SpinnerIcon class="animate-spin" />
|
||||||
|
<span>Loading changelog...</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="mb-3 flex">
|
||||||
|
<VersionFilterControl
|
||||||
|
:versions="versions ?? []"
|
||||||
|
:game-versions="tags.gameVersions"
|
||||||
|
@update:query="updateQuery"
|
||||||
|
/>
|
||||||
|
<Pagination
|
||||||
|
:page="currentPage"
|
||||||
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
|
class="ml-auto mt-auto"
|
||||||
|
:link-function="(page) => `?page=${page}`"
|
||||||
|
@switch-page="switchPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="card changelog-wrapper">
|
||||||
|
<template v-if="paginatedVersions && !isLoadingVersions">
|
||||||
|
<div v-for="version in paginatedVersions" :key="version.id" class="changelog-item">
|
||||||
|
<div
|
||||||
|
:class="`changelog-bar ${version.version_type} ${version.duplicate ? 'duplicate' : ''}`"
|
||||||
|
/>
|
||||||
|
<div class="version-wrapper">
|
||||||
|
<div class="version-header">
|
||||||
|
<div class="version-header-text">
|
||||||
|
<h2 class="name">
|
||||||
|
<nuxt-link
|
||||||
|
:to="`/${projectV2.project_type}/${
|
||||||
|
projectV2.slug ? projectV2.slug : projectV2.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`"
|
||||||
|
>
|
||||||
|
{{ version.name }}
|
||||||
|
</nuxt-link>
|
||||||
|
</h2>
|
||||||
|
<span v-if="version.author">
|
||||||
|
by
|
||||||
|
<nuxt-link class="text-link" :to="'/user/' + version.author.user.username">{{
|
||||||
|
version.author.user.username
|
||||||
|
}}</nuxt-link>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
on
|
||||||
|
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
:href="version.primaryFile?.url"
|
||||||
|
class="iconified-button download"
|
||||||
|
:title="`Download ${version.name}`"
|
||||||
|
>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="version.changelog && !version.duplicate"
|
||||||
|
class="markdown-body"
|
||||||
|
v-html="renderHighlightedString(version.changelog)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<SpinnerIcon class="animate-spin" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<Pagination
|
<Pagination
|
||||||
:page="currentPage"
|
:page="currentPage"
|
||||||
:count="Math.ceil(filteredVersions.length / 20)"
|
:count="Math.ceil(filteredVersions.length / 20)"
|
||||||
class="ml-auto mt-auto"
|
class="mb-2 flex justify-end"
|
||||||
:link-function="(page) => `?page=${page}`"
|
:link-function="(page) => `?page=${page}`"
|
||||||
@switch-page="switchPage"
|
@switch-page="switchPage"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
<div class="card changelog-wrapper">
|
|
||||||
<template v-if="paginatedVersions && !isLoadingVersions">
|
|
||||||
<div v-for="version in paginatedVersions" :key="version.id" class="changelog-item">
|
|
||||||
<div
|
|
||||||
:class="`changelog-bar ${version.version_type} ${version.duplicate ? 'duplicate' : ''}`"
|
|
||||||
/>
|
|
||||||
<div class="version-wrapper">
|
|
||||||
<div class="version-header">
|
|
||||||
<div class="version-header-text">
|
|
||||||
<h2 class="name">
|
|
||||||
<nuxt-link
|
|
||||||
:to="`/${props.project.project_type}/${
|
|
||||||
props.project.slug ? props.project.slug : props.project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}`"
|
|
||||||
>
|
|
||||||
{{ version.name }}
|
|
||||||
</nuxt-link>
|
|
||||||
</h2>
|
|
||||||
<span v-if="version.author">
|
|
||||||
by
|
|
||||||
<nuxt-link class="text-link" :to="'/user/' + version.author.user.username">{{
|
|
||||||
version.author.user.username
|
|
||||||
}}</nuxt-link>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
on
|
|
||||||
{{ $dayjs(version.date_published).format('MMM D, YYYY') }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
:href="version.primaryFile?.url"
|
|
||||||
class="iconified-button download"
|
|
||||||
:title="`Download ${version.name}`"
|
|
||||||
>
|
|
||||||
<DownloadIcon aria-hidden="true" />
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="version.changelog && !version.duplicate"
|
|
||||||
class="markdown-body"
|
|
||||||
v-html="renderHighlightedString(version.changelog)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<SpinnerIcon class="animate-spin" />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<Pagination
|
|
||||||
:page="currentPage"
|
|
||||||
:count="Math.ceil(filteredVersions.length / 20)"
|
|
||||||
class="mb-2 flex justify-end"
|
|
||||||
:link-function="(page) => `?page=${page}`"
|
|
||||||
@switch-page="switchPage"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { DownloadIcon, SpinnerIcon } from '@modrinth/assets'
|
import { DownloadIcon, SpinnerIcon } from '@modrinth/assets'
|
||||||
import { injectModrinthClient, Pagination } from '@modrinth/ui'
|
import { injectModrinthClient, injectProjectPageContext, Pagination } from '@modrinth/ui'
|
||||||
import VersionFilterControl from '@modrinth/ui/src/components/version/VersionFilterControl.vue'
|
import VersionFilterControl from '@modrinth/ui/src/components/version/VersionFilterControl.vue'
|
||||||
import { renderHighlightedString } from '@modrinth/utils'
|
import { renderHighlightedString } from '@modrinth/utils'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const { projectV2, versions, versionsLoading, loadVersions } = injectProjectPageContext()
|
||||||
project: {
|
|
||||||
type: Object,
|
// Load versions on mount (client-side)
|
||||||
default() {
|
onMounted(() => {
|
||||||
return {}
|
loadVersions()
|
||||||
},
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = `${props.project.title} - Changelog`
|
const title = computed(() => `${projectV2.value.title} - Changelog`)
|
||||||
const description = `View the changelog of ${props.project.title}'s ${props.versions.length} versions.`
|
const description = computed(
|
||||||
|
() => `View the changelog of ${projectV2.value.title}'s ${versions.value?.length ?? 0} versions.`,
|
||||||
|
)
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title,
|
title,
|
||||||
@@ -117,11 +117,13 @@ const tags = useGeneratedState()
|
|||||||
|
|
||||||
const currentPage = ref(Number(route.query.page ?? 1))
|
const currentPage = ref(Number(route.query.page ?? 1))
|
||||||
const filteredVersions = computed(() => {
|
const filteredVersions = computed(() => {
|
||||||
|
if (!versions.value) return []
|
||||||
|
|
||||||
const selectedGameVersions = getArrayOrString(route.query.g) ?? []
|
const selectedGameVersions = getArrayOrString(route.query.g) ?? []
|
||||||
const selectedLoaders = getArrayOrString(route.query.l) ?? []
|
const selectedLoaders = getArrayOrString(route.query.l) ?? []
|
||||||
const selectedVersionTypes = getArrayOrString(route.query.c) ?? []
|
const selectedVersionTypes = getArrayOrString(route.query.c) ?? []
|
||||||
|
|
||||||
return props.versions.filter(
|
return versions.value.filter(
|
||||||
(projectVersion) =>
|
(projectVersion) =>
|
||||||
(selectedGameVersions.length === 0 ||
|
(selectedGameVersions.length === 0 ||
|
||||||
selectedGameVersions.some((gameVersion) =>
|
selectedGameVersions.some((gameVersion) =>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
:src="
|
:src="
|
||||||
previewImage
|
previewImage
|
||||||
? previewImage
|
? previewImage
|
||||||
: project.gallery[editIndex] && project.gallery[editIndex].url
|
: project.gallery?.[editIndex]?.url
|
||||||
? project.gallery[editIndex].url
|
? project.gallery[editIndex].url
|
||||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||||
"
|
"
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
Unfeature image
|
Unfeature image
|
||||||
</button>
|
</button>
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
<button class="iconified-button" @click="$refs.modal_edit_item.hide()">
|
<button class="iconified-button" @click="modalEditItem?.hide()">
|
||||||
<XIcon aria-hidden="true" />
|
<XIcon aria-hidden="true" />
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -165,8 +165,8 @@
|
|||||||
class="open circle-button"
|
class="open circle-button"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
:href="
|
:href="
|
||||||
expandedGalleryItem.raw_url
|
expandedGalleryItem?.raw_url
|
||||||
? expandedGalleryItem.raw_url
|
? expandedGalleryItem?.raw_url
|
||||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -177,14 +177,14 @@
|
|||||||
<ContractIcon v-else aria-hidden="true" />
|
<ContractIcon v-else aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="project.gallery.length > 1"
|
v-if="(project?.gallery?.length ?? 0) > 1"
|
||||||
class="previous circle-button"
|
class="previous circle-button"
|
||||||
@click="previousImage()"
|
@click="previousImage()"
|
||||||
>
|
>
|
||||||
<LeftArrowIcon aria-hidden="true" />
|
<LeftArrowIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="project.gallery.length > 1"
|
v-if="(project?.gallery?.length ?? 0) > 1"
|
||||||
class="next circle-button"
|
class="next circle-button"
|
||||||
@click="nextImage()"
|
@click="nextImage()"
|
||||||
>
|
>
|
||||||
@@ -206,7 +206,7 @@
|
|||||||
<button
|
<button
|
||||||
aria-label="Project Settings"
|
aria-label="Project Settings"
|
||||||
class="!shadow-none"
|
class="!shadow-none"
|
||||||
@click="() => $router.push('settings/gallery')"
|
@click="() => router.push('settings/gallery')"
|
||||||
>
|
>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
Edit gallery
|
Edit gallery
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Admonition>
|
</Admonition>
|
||||||
<div v-if="currentMember && project.gallery.length" class="card header-buttons">
|
<div v-if="currentMember && project?.gallery?.length" class="card header-buttons">
|
||||||
<FileInput
|
<FileInput
|
||||||
:max-size="5242880"
|
:max-size="5242880"
|
||||||
:accept="acceptFileTypes"
|
:accept="acceptFileTypes"
|
||||||
@@ -245,9 +245,9 @@
|
|||||||
@change="handleFiles"
|
@change="handleFiles"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="project.gallery.length" class="items">
|
<div v-if="project?.gallery?.length" class="items">
|
||||||
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
<div v-for="(item, index) in project.gallery" :key="index" class="card gallery-item">
|
||||||
<a class="gallery-thumbnail" @click="expandImage(item, index)">
|
<a class="gallery-thumbnail" @click="expandImage(item as GalleryItem, index)">
|
||||||
<img
|
<img
|
||||||
:src="item.url ? item.url : 'https://cdn.modrinth.com/placeholder-banner.svg'"
|
:src="item.url ? item.url : 'https://cdn.modrinth.com/placeholder-banner.svg'"
|
||||||
:alt="item.title ? item.title : 'gallery-image'"
|
:alt="item.title ? item.title : 'gallery-image'"
|
||||||
@@ -275,11 +275,11 @@
|
|||||||
() => {
|
() => {
|
||||||
resetEdit()
|
resetEdit()
|
||||||
editIndex = index
|
editIndex = index
|
||||||
editTitle = item.title
|
editTitle = item.title ?? ''
|
||||||
editDescription = item.description
|
editDescription = item.description ?? ''
|
||||||
editFeatured = item.featured
|
editFeatured = item.featured
|
||||||
editOrder = item.ordering
|
editOrder = item.ordering
|
||||||
$refs.modal_edit_item.show()
|
modalEditItem?.show()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -291,7 +291,7 @@
|
|||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
deleteIndex = index
|
deleteIndex = index
|
||||||
$refs.modal_confirm.show()
|
modalConfirm?.show()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -314,7 +314,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
ContractIcon,
|
ContractIcon,
|
||||||
@@ -341,34 +341,29 @@ import {
|
|||||||
DropArea,
|
DropArea,
|
||||||
FileInput,
|
FileInput,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
NewModal as Modal,
|
NewModal as Modal,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useLocalStorage } from '@vueuse/core'
|
import { useEventListener, useLocalStorage } from '@vueuse/core'
|
||||||
|
|
||||||
import { isPermission } from '~/utils/permissions.ts'
|
import { isPermission } from '~/utils/permissions.ts'
|
||||||
|
|
||||||
const props = defineProps({
|
// Router
|
||||||
project: {
|
const router = useRouter()
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resetProject: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = `${props.project.title} - Gallery`
|
// Single DI injection
|
||||||
const description = `View ${props.project.gallery.length} images of ${props.project.title} on Modrinth.`
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { projectV2: project, currentMember, refreshProject } = injectProjectPageContext()
|
||||||
|
|
||||||
|
// Template refs
|
||||||
|
const modalEditItem = useTemplateRef('modal_edit_item')
|
||||||
|
const modalConfirm = useTemplateRef('modal_confirm')
|
||||||
|
|
||||||
|
// SEO
|
||||||
|
const title = computed(() => `${project.value.title} - Gallery`)
|
||||||
|
const description = computed(
|
||||||
|
() => `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.`,
|
||||||
|
)
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title,
|
title,
|
||||||
@@ -377,207 +372,219 @@ useSeoMeta({
|
|||||||
ogDescription: description,
|
ogDescription: description,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Local storage state
|
||||||
const hideGalleryAdmonition = useLocalStorage(
|
const hideGalleryAdmonition = useLocalStorage(
|
||||||
'hideGalleryHasMovedAdmonition',
|
'hideGalleryHasMovedAdmonition',
|
||||||
!props.project.gallery.length,
|
!project.value.gallery?.length,
|
||||||
)
|
)
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
// Gallery item type matching actual v2 API response (LegacyGalleryItem in labrinth)
|
||||||
export default defineNuxtComponent({
|
// raw_url is optional in TS types but present in API response
|
||||||
setup() {
|
interface GalleryItem {
|
||||||
const { addNotification } = injectNotificationManager()
|
url: string
|
||||||
|
raw_url?: string
|
||||||
|
featured: boolean
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
created: string
|
||||||
|
ordering: number
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
// Expanded image modal state
|
||||||
addNotification,
|
const expandedGalleryItem = ref<GalleryItem | null>(null)
|
||||||
|
const expandedGalleryIndex = ref(0)
|
||||||
|
const zoomedIn = ref(false)
|
||||||
|
|
||||||
|
// Delete state
|
||||||
|
const deleteIndex = ref(-1)
|
||||||
|
|
||||||
|
// Edit state
|
||||||
|
const editIndex = ref(-1)
|
||||||
|
const editTitle = ref('')
|
||||||
|
const editDescription = ref('')
|
||||||
|
const editFeatured = ref(false)
|
||||||
|
const editOrder = ref<number | null>(null)
|
||||||
|
const editFile = ref<File | null>(null)
|
||||||
|
const previewImage = ref<string | null>(null)
|
||||||
|
|
||||||
|
// UI state
|
||||||
|
const shouldPreventActions = ref(false)
|
||||||
|
|
||||||
|
// Constant for accepted file types
|
||||||
|
const acceptFileTypes = 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
|
||||||
|
|
||||||
|
// Keyboard navigation for expanded image modal
|
||||||
|
useEventListener(document, 'keydown', (e) => {
|
||||||
|
if (expandedGalleryItem.value) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
expandedGalleryItem.value = null
|
||||||
|
} else if (e.key === 'ArrowLeft') {
|
||||||
|
e.stopPropagation()
|
||||||
|
previousImage()
|
||||||
|
} else if (e.key === 'ArrowRight') {
|
||||||
|
e.stopPropagation()
|
||||||
|
nextImage()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
expandedGalleryItem: null,
|
|
||||||
expandedGalleryIndex: 0,
|
|
||||||
zoomedIn: false,
|
|
||||||
|
|
||||||
deleteIndex: -1,
|
|
||||||
|
|
||||||
editIndex: -1,
|
|
||||||
editTitle: '',
|
|
||||||
editDescription: '',
|
|
||||||
editFeatured: false,
|
|
||||||
editOrder: null,
|
|
||||||
editFile: null,
|
|
||||||
previewImage: null,
|
|
||||||
shouldPreventActions: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
acceptFileTypes() {
|
|
||||||
return 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this._keyListener = function (e) {
|
|
||||||
if (this.expandedGalleryItem) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
this.expandedGalleryItem = null
|
|
||||||
} else if (e.key === 'ArrowLeft') {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.previousImage()
|
|
||||||
} else if (e.key === 'ArrowRight') {
|
|
||||||
e.stopPropagation()
|
|
||||||
this.nextImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', this._keyListener.bind(this))
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
nextImage() {
|
|
||||||
this.expandedGalleryIndex++
|
|
||||||
if (this.expandedGalleryIndex >= this.project.gallery.length) {
|
|
||||||
this.expandedGalleryIndex = 0
|
|
||||||
}
|
|
||||||
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
|
|
||||||
},
|
|
||||||
previousImage() {
|
|
||||||
this.expandedGalleryIndex--
|
|
||||||
if (this.expandedGalleryIndex < 0) {
|
|
||||||
this.expandedGalleryIndex = this.project.gallery.length - 1
|
|
||||||
}
|
|
||||||
this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
|
|
||||||
},
|
|
||||||
expandImage(item, index) {
|
|
||||||
this.expandedGalleryItem = item
|
|
||||||
this.expandedGalleryIndex = index
|
|
||||||
this.zoomedIn = false
|
|
||||||
},
|
|
||||||
resetEdit() {
|
|
||||||
this.editIndex = -1
|
|
||||||
this.editTitle = ''
|
|
||||||
this.editDescription = ''
|
|
||||||
this.editFeatured = false
|
|
||||||
this.editOrder = null
|
|
||||||
this.editFile = null
|
|
||||||
this.previewImage = null
|
|
||||||
},
|
|
||||||
handleFiles(files) {
|
|
||||||
this.resetEdit()
|
|
||||||
this.editFile = files[0]
|
|
||||||
|
|
||||||
this.showPreviewImage()
|
|
||||||
this.$refs.modal_edit_item.show()
|
|
||||||
},
|
|
||||||
showPreviewImage() {
|
|
||||||
const reader = new FileReader()
|
|
||||||
if (this.editFile instanceof Blob) {
|
|
||||||
reader.readAsDataURL(this.editFile)
|
|
||||||
reader.onload = (event) => {
|
|
||||||
this.previewImage = event.target.result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async createGalleryItem() {
|
|
||||||
this.shouldPreventActions = true
|
|
||||||
startLoading()
|
|
||||||
|
|
||||||
try {
|
|
||||||
let url = `project/${this.project.id}/gallery?ext=${
|
|
||||||
this.editFile
|
|
||||||
? this.editFile.type.split('/')[this.editFile.type.split('/').length - 1]
|
|
||||||
: null
|
|
||||||
}&featured=${this.editFeatured}`
|
|
||||||
|
|
||||||
if (this.editTitle) {
|
|
||||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
|
||||||
}
|
|
||||||
if (this.editDescription) {
|
|
||||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
|
||||||
}
|
|
||||||
if (this.editOrder) {
|
|
||||||
url += `&ordering=${this.editOrder}`
|
|
||||||
}
|
|
||||||
|
|
||||||
await useBaseFetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
body: this.editFile,
|
|
||||||
})
|
|
||||||
await this.resetProject()
|
|
||||||
|
|
||||||
this.$refs.modal_edit_item.hide()
|
|
||||||
} catch (err) {
|
|
||||||
this.addNotification({
|
|
||||||
title: 'An error occurred',
|
|
||||||
text: err.data ? err.data.description : err,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLoading()
|
|
||||||
this.shouldPreventActions = false
|
|
||||||
},
|
|
||||||
async editGalleryItem() {
|
|
||||||
this.shouldPreventActions = true
|
|
||||||
startLoading()
|
|
||||||
try {
|
|
||||||
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
|
|
||||||
this.project.gallery[this.editIndex].url,
|
|
||||||
)}&featured=${this.editFeatured}`
|
|
||||||
|
|
||||||
if (this.editTitle) {
|
|
||||||
url += `&title=${encodeURIComponent(this.editTitle)}`
|
|
||||||
}
|
|
||||||
if (this.editDescription) {
|
|
||||||
url += `&description=${encodeURIComponent(this.editDescription)}`
|
|
||||||
}
|
|
||||||
if (this.editOrder) {
|
|
||||||
url += `&ordering=${this.editOrder}`
|
|
||||||
}
|
|
||||||
|
|
||||||
await useBaseFetch(url, {
|
|
||||||
method: 'PATCH',
|
|
||||||
})
|
|
||||||
|
|
||||||
await this.resetProject()
|
|
||||||
this.$refs.modal_edit_item.hide()
|
|
||||||
} catch (err) {
|
|
||||||
this.addNotification({
|
|
||||||
title: 'An error occurred',
|
|
||||||
text: err.data ? err.data.description : err,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLoading()
|
|
||||||
this.shouldPreventActions = false
|
|
||||||
},
|
|
||||||
async deleteGalleryImage() {
|
|
||||||
startLoading()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await useBaseFetch(
|
|
||||||
`project/${this.project.id}/gallery?url=${encodeURIComponent(
|
|
||||||
this.project.gallery[this.deleteIndex].url,
|
|
||||||
)}`,
|
|
||||||
{
|
|
||||||
method: 'DELETE',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
await this.resetProject()
|
|
||||||
} catch (err) {
|
|
||||||
this.addNotification({
|
|
||||||
title: 'An error occurred',
|
|
||||||
text: err.data ? err.data.description : err,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLoading()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Navigation functions
|
||||||
|
function nextImage() {
|
||||||
|
expandedGalleryIndex.value++
|
||||||
|
if (expandedGalleryIndex.value >= project.value.gallery!.length) {
|
||||||
|
expandedGalleryIndex.value = 0
|
||||||
|
}
|
||||||
|
expandedGalleryItem.value = project.value.gallery![expandedGalleryIndex.value] as GalleryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousImage() {
|
||||||
|
expandedGalleryIndex.value--
|
||||||
|
if (expandedGalleryIndex.value < 0) {
|
||||||
|
expandedGalleryIndex.value = project.value.gallery!.length - 1
|
||||||
|
}
|
||||||
|
expandedGalleryItem.value = project.value.gallery![expandedGalleryIndex.value] as GalleryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandImage(item: GalleryItem, index: number) {
|
||||||
|
expandedGalleryItem.value = item
|
||||||
|
expandedGalleryIndex.value = index
|
||||||
|
zoomedIn.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit state management
|
||||||
|
function resetEdit() {
|
||||||
|
editIndex.value = -1
|
||||||
|
editTitle.value = ''
|
||||||
|
editDescription.value = ''
|
||||||
|
editFeatured.value = false
|
||||||
|
editOrder.value = null
|
||||||
|
editFile.value = null
|
||||||
|
previewImage.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFiles(files: File[]) {
|
||||||
|
resetEdit()
|
||||||
|
editFile.value = files[0]
|
||||||
|
|
||||||
|
showPreviewImage()
|
||||||
|
modalEditItem.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreviewImage() {
|
||||||
|
const reader = new FileReader()
|
||||||
|
if (editFile.value instanceof Blob) {
|
||||||
|
reader.readAsDataURL(editFile.value)
|
||||||
|
reader.onload = (event) => {
|
||||||
|
previewImage.value = event.target?.result as string | null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUD operations
|
||||||
|
async function createGalleryItem() {
|
||||||
|
shouldPreventActions.value = true
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
let url = `project/${project.value.id}/gallery?ext=${
|
||||||
|
editFile.value
|
||||||
|
? editFile.value.type.split('/')[editFile.value.type.split('/').length - 1]
|
||||||
|
: null
|
||||||
|
}&featured=${editFeatured.value}`
|
||||||
|
|
||||||
|
if (editTitle.value) {
|
||||||
|
url += `&title=${encodeURIComponent(editTitle.value)}`
|
||||||
|
}
|
||||||
|
if (editDescription.value) {
|
||||||
|
url += `&description=${encodeURIComponent(editDescription.value)}`
|
||||||
|
}
|
||||||
|
if (editOrder.value) {
|
||||||
|
url += `&ordering=${editOrder.value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
await useBaseFetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: editFile.value,
|
||||||
|
})
|
||||||
|
await refreshProject()
|
||||||
|
|
||||||
|
modalEditItem.value?.hide()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { data?: { description?: string } }
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: error.data?.description ?? String(err),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
shouldPreventActions.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function editGalleryItem() {
|
||||||
|
shouldPreventActions.value = true
|
||||||
|
startLoading()
|
||||||
|
try {
|
||||||
|
let url = `project/${project.value.id}/gallery?url=${encodeURIComponent(
|
||||||
|
project.value!.gallery![editIndex.value].url,
|
||||||
|
)}&featured=${editFeatured.value}`
|
||||||
|
|
||||||
|
if (editTitle.value) {
|
||||||
|
url += `&title=${encodeURIComponent(editTitle.value)}`
|
||||||
|
}
|
||||||
|
if (editDescription.value) {
|
||||||
|
url += `&description=${encodeURIComponent(editDescription.value)}`
|
||||||
|
}
|
||||||
|
if (editOrder.value) {
|
||||||
|
url += `&ordering=${editOrder.value}`
|
||||||
|
}
|
||||||
|
|
||||||
|
await useBaseFetch(url, {
|
||||||
|
method: 'PATCH',
|
||||||
|
})
|
||||||
|
|
||||||
|
await refreshProject()
|
||||||
|
modalEditItem.value?.hide()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { data?: { description?: string } }
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: error.data?.description ?? String(err),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
shouldPreventActions.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGalleryImage() {
|
||||||
|
startLoading()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await useBaseFetch(
|
||||||
|
`project/${project.value.id}/gallery?url=${encodeURIComponent(
|
||||||
|
project.value!.gallery![deleteIndex.value].url!,
|
||||||
|
)}`,
|
||||||
|
{
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
await refreshProject()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const error = err as { data?: { description?: string } }
|
||||||
|
addNotification({
|
||||||
|
title: 'An error occurred',
|
||||||
|
text: error.data?.description ?? String(err),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stopLoading()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="normal-page__content">
|
<section class="normal-page__content">
|
||||||
<div v-if="project.body" class="card">
|
<div v-if="projectV2.body" class="card">
|
||||||
<ProjectPageDescription :description="project.body" />
|
<ProjectPageDescription :description="projectV2.body" />
|
||||||
</div>
|
</div>
|
||||||
<p v-else class="ml-2">
|
<p v-else class="ml-2">
|
||||||
No description provided. Visit
|
No description provided. Visit
|
||||||
@@ -14,34 +14,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ProjectPageDescription } from '@modrinth/ui'
|
import { injectProjectPageContext, ProjectPageDescription } from '@modrinth/ui'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
defineProps({
|
const { projectV2 } = injectProjectPageContext()
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
organization: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { CheckIcon, IssuesIcon, XIcon } from '@modrinth/assets'
|
import { CheckIcon, IssuesIcon, XIcon } from '@modrinth/assets'
|
||||||
import { Badge, injectNotificationManager } from '@modrinth/ui'
|
import { Badge, injectNotificationManager, injectProjectPageContext } from '@modrinth/ui'
|
||||||
|
|
||||||
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
import ConversationThread from '~/components/ui/thread/ConversationThread.vue'
|
||||||
import {
|
import {
|
||||||
@@ -113,45 +113,28 @@ import {
|
|||||||
} from '~/helpers/projects.js'
|
} from '~/helpers/projects.js'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const props = defineProps({
|
const { projectV2: project, currentMember, refreshProject } = injectProjectPageContext()
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resetProject: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
|
|
||||||
const { data: thread } = await useAsyncData(`thread/${props.project.thread_id}`, () =>
|
const { data: thread } = await useAsyncData(
|
||||||
useBaseFetch(`thread/${props.project.thread_id}`),
|
() => `thread/${project.value.thread_id}`,
|
||||||
|
() => useBaseFetch(`thread/${project.value.thread_id}`),
|
||||||
)
|
)
|
||||||
|
|
||||||
async function setStatus(status) {
|
async function setStatus(status) {
|
||||||
startLoading()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = {}
|
const data = {}
|
||||||
data.status = status
|
data.status = status
|
||||||
await useBaseFetch(`project/${props.project.id}`, {
|
await useBaseFetch(`project/${project.value.id}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: data,
|
body: data,
|
||||||
})
|
})
|
||||||
|
|
||||||
const project = props.project
|
project.value.status = status
|
||||||
project.status = status
|
await refreshProject()
|
||||||
await props.resetProject()
|
|
||||||
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
|
thread.value = await useBaseFetch(`thread/${thread.value.id}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
addNotification({
|
addNotification({
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import {
|
|||||||
import {
|
import {
|
||||||
commonMessages,
|
commonMessages,
|
||||||
commonProjectSettingsMessages,
|
commonProjectSettingsMessages,
|
||||||
injectNotificationManager,
|
injectProjectPageContext,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { isStaff, type Project, type ProjectV3Partial } from '@modrinth/utils'
|
import { isStaff } from '@modrinth/utils'
|
||||||
import { useLocalStorage, useScroll } from '@vueuse/core'
|
import { useLocalStorage, useScroll } from '@vueuse/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
@@ -26,32 +26,22 @@ import NavStack from '~/components/ui/NavStack.vue'
|
|||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const props = defineProps<{
|
const {
|
||||||
currentMember: any
|
projectV2: project,
|
||||||
patchProject: any
|
projectV3,
|
||||||
patchIcon: any
|
versions,
|
||||||
resetProject: any
|
currentMember,
|
||||||
resetVersions: any
|
setProcessing,
|
||||||
resetOrganization: any
|
} = injectProjectPageContext()
|
||||||
resetMembers: any
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
|
|
||||||
const project = defineModel<Project>('project', { required: true })
|
|
||||||
const projectV3 = defineModel<ProjectV3Partial>('projectV3', { required: true })
|
|
||||||
const versions = defineModel<any>('versions')
|
|
||||||
const members = defineModel<any>('members')
|
|
||||||
const allMembers = defineModel<any>('allMembers')
|
|
||||||
const dependencies = defineModel<any>('dependencies')
|
|
||||||
const organization = defineModel<any>('organization')
|
|
||||||
|
|
||||||
const navItems = computed(() => {
|
const navItems = computed(() => {
|
||||||
const base = `${project.value.project_type}/${project.value.slug ? project.value.slug : project.value.id}`
|
const base = `${project.value.project_type}/${project.value.slug ? project.value.slug : project.value.id}`
|
||||||
|
|
||||||
const showEnvironment =
|
const showEnvironment =
|
||||||
projectV3.value.project_types.some((type) => ['mod', 'modpack'].includes(type)) &&
|
projectV3.value?.project_types?.some((type) => ['mod', 'modpack'].includes(type)) &&
|
||||||
isStaff(props.currentMember?.user)
|
isStaff(currentMember.value?.user)
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -118,35 +108,10 @@ const navItems = computed(() => {
|
|||||||
return items.filter(Boolean) as any[]
|
return items.filter(Boolean) as any[]
|
||||||
})
|
})
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
|
||||||
|
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const collapsedChecklist = useLocalStorage(`project-checklist-collapsed-${project.value.id}`, false)
|
const collapsedChecklist = useLocalStorage(`project-checklist-collapsed-${project.value.id}`, false)
|
||||||
|
|
||||||
async function setProcessing() {
|
|
||||||
startLoading()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await useBaseFetch(`project/${project.value.id}`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
body: {
|
|
||||||
status: 'processing',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
project.value.status = 'processing'
|
|
||||||
} catch (err: any) {
|
|
||||||
addNotification({
|
|
||||||
title: formatMessage(commonMessages.errorNotificationTitle),
|
|
||||||
text: err.data ? err.data.description : err,
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLoading()
|
|
||||||
}
|
|
||||||
|
|
||||||
// To persist scroll position through settings pages
|
// To persist scroll position through settings pages
|
||||||
// This scroll code is jank asf, if anyone has a better way please do suggest it
|
// This scroll code is jank asf, if anyone has a better way please do suggest it
|
||||||
const scroll = useScroll(window)
|
const scroll = useScroll(window)
|
||||||
@@ -167,7 +132,7 @@ watch(route, () => {
|
|||||||
:versions="versions"
|
:versions="versions"
|
||||||
:current-member="currentMember"
|
:current-member="currentMember"
|
||||||
:collapsed="collapsedChecklist"
|
:collapsed="collapsedChecklist"
|
||||||
:route-name="route.name as string"
|
:route-name="route.name"
|
||||||
:tags="tags"
|
:tags="tags"
|
||||||
@toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
@toggle-collapsed="() => (collapsedChecklist = !collapsedChecklist)"
|
||||||
@set-processing="setProcessing"
|
@set-processing="setProcessing"
|
||||||
@@ -177,22 +142,7 @@ watch(route, () => {
|
|||||||
<NavStack :items="navItems" />
|
<NavStack :items="navItems" />
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<NuxtPage
|
<NuxtPage />
|
||||||
v-model:project="project"
|
|
||||||
v-model:project-v3="projectV3"
|
|
||||||
v-model:versions="versions"
|
|
||||||
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"
|
|
||||||
:reset-project="resetProject"
|
|
||||||
:reset-versions="resetVersions"
|
|
||||||
:reset-organization="resetOrganization"
|
|
||||||
:reset-members="resetMembers"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,21 +11,16 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ChartDisplay :projects="[props.project]" />
|
<ChartDisplay :projects="[project]" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { injectProjectPageContext } from '@modrinth/ui'
|
||||||
|
|
||||||
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
|
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const { projectV2: project } = injectProjectPageContext()
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
v-model="description"
|
v-model="description"
|
||||||
:disabled="
|
:disabled="
|
||||||
!currentMember ||
|
!currentMember ||
|
||||||
(currentMember.permissions & TeamMemberPermission.EDIT_BODY) !==
|
(currentMember?.permissions! & TeamMemberPermission.EDIT_BODY) !==
|
||||||
TeamMemberPermission.EDIT_BODY
|
TeamMemberPermission.EDIT_BODY
|
||||||
"
|
"
|
||||||
:on-image-upload="onUploadHandler"
|
:on-image-upload="onUploadHandler"
|
||||||
@@ -44,20 +44,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { SaveIcon, TriangleAlertIcon } from '@modrinth/assets'
|
import { SaveIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||||
import { countText, MIN_DESCRIPTION_CHARS } from '@modrinth/moderation'
|
import { countText, MIN_DESCRIPTION_CHARS } from '@modrinth/moderation'
|
||||||
import { MarkdownEditor } from '@modrinth/ui'
|
import { injectProjectPageContext, MarkdownEditor } from '@modrinth/ui'
|
||||||
import { type Project, type TeamMember, TeamMemberPermission } from '@modrinth/utils'
|
import { TeamMemberPermission } from '@modrinth/utils'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
import { useImageUpload } from '~/composables/image-upload.ts'
|
import { useImageUpload } from '~/composables/image-upload.ts'
|
||||||
|
|
||||||
const props = defineProps<{
|
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
|
||||||
project: Project
|
|
||||||
allMembers: TeamMember[]
|
|
||||||
currentMember: TeamMember | undefined
|
|
||||||
patchProject: (payload: object, quiet?: boolean) => object
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const description = ref(props.project.body)
|
const description = ref(project.value.body)
|
||||||
|
|
||||||
const descriptionWarning = computed(() => {
|
const descriptionWarning = computed(() => {
|
||||||
const text = description.value?.trim() || ''
|
const text = description.value?.trim() || ''
|
||||||
@@ -75,7 +70,7 @@ const patchRequestPayload = computed(() => {
|
|||||||
body?: string
|
body?: string
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (description.value !== props.project.body) {
|
if (description.value !== project.value.body) {
|
||||||
payload.body = description.value
|
payload.body = description.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,13 +82,13 @@ const hasChanges = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
props.patchProject(patchRequestPayload.value)
|
patchProject(patchRequestPayload.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onUploadHandler(file: File) {
|
async function onUploadHandler(file: File) {
|
||||||
const response = await useImageUpload(file, {
|
const response = await useImageUpload(file, {
|
||||||
context: 'project',
|
context: 'project',
|
||||||
projectID: props.project.id,
|
projectID: project.value.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.url
|
return response.url
|
||||||
|
|||||||
@@ -300,33 +300,16 @@ import {
|
|||||||
DropArea,
|
DropArea,
|
||||||
FileInput,
|
FileInput,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
NewModal as Modal,
|
NewModal as Modal,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
|
||||||
import { isPermission } from '~/utils/permissions.ts'
|
import { isPermission } from '~/utils/permissions.ts'
|
||||||
|
|
||||||
const props = defineProps({
|
const { projectV2: project, currentMember } = injectProjectPageContext()
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resetProject: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const title = `${props.project.title} - Gallery`
|
const title = `${project.value.title} - Gallery`
|
||||||
const description = `View ${props.project.gallery.length} images of ${props.project.title} on Modrinth.`
|
const description = `View ${project.value.gallery?.length ?? 0} images of ${project.value.title} on Modrinth.`
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title,
|
title,
|
||||||
@@ -340,9 +323,12 @@ useSeoMeta({
|
|||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
setup() {
|
setup() {
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const { projectV2: project, refreshProject } = injectProjectPageContext()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
addNotification,
|
addNotification,
|
||||||
|
project,
|
||||||
|
refreshProject,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -456,7 +442,7 @@ export default defineNuxtComponent({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: this.editFile,
|
body: this.editFile,
|
||||||
})
|
})
|
||||||
await this.resetProject()
|
await this.refreshProject()
|
||||||
|
|
||||||
this.$refs.modal_edit_item.hide()
|
this.$refs.modal_edit_item.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -492,7 +478,7 @@ export default defineNuxtComponent({
|
|||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.resetProject()
|
await this.refreshProject()
|
||||||
this.$refs.modal_edit_item.hide()
|
this.$refs.modal_edit_item.hide()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.addNotification({
|
this.addNotification({
|
||||||
@@ -518,7 +504,7 @@ export default defineNuxtComponent({
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
await this.resetProject()
|
await this.refreshProject()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.addNotification({
|
this.addNotification({
|
||||||
title: 'An error occurred',
|
title: 'An error occurred',
|
||||||
|
|||||||
@@ -256,7 +256,12 @@ import {
|
|||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { MIN_SUMMARY_CHARS } from '@modrinth/moderation'
|
import { MIN_SUMMARY_CHARS } from '@modrinth/moderation'
|
||||||
import { Avatar, ConfirmModal, injectNotificationManager } from '@modrinth/ui'
|
import {
|
||||||
|
Avatar,
|
||||||
|
ConfirmModal,
|
||||||
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
|
} from '@modrinth/ui'
|
||||||
import { formatProjectStatus, formatProjectType } from '@modrinth/utils'
|
import { formatProjectStatus, formatProjectType } from '@modrinth/utils'
|
||||||
import { Multiselect } from 'vue-multiselect'
|
import { Multiselect } from 'vue-multiselect'
|
||||||
|
|
||||||
@@ -264,67 +269,41 @@ import FileInput from '~/components/ui/FileInput.vue'
|
|||||||
import { useFeatureFlags } from '~/composables/featureFlags.ts'
|
import { useFeatureFlags } from '~/composables/featureFlags.ts'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const {
|
||||||
|
projectV2: project,
|
||||||
|
currentMember,
|
||||||
|
patchProject,
|
||||||
|
patchIcon,
|
||||||
|
refreshProject,
|
||||||
|
} = injectProjectPageContext()
|
||||||
|
|
||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
projectV3: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
patchProject: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
patchIcon: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
resetProject: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
const router = useNativeRouter()
|
const router = useNativeRouter()
|
||||||
|
|
||||||
const name = ref(props.project.title)
|
const name = ref(project.value.title)
|
||||||
const slug = ref(props.project.slug)
|
const slug = ref(project.value.slug)
|
||||||
const summary = ref(props.project.description)
|
const summary = ref(project.value.description)
|
||||||
const icon = ref(null)
|
const icon = ref(null)
|
||||||
const previewImage = ref(null)
|
const previewImage = ref(null)
|
||||||
const clientSide = ref(props.project.client_side)
|
const clientSide = ref(project.value.client_side)
|
||||||
const serverSide = ref(props.project.server_side)
|
const serverSide = ref(project.value.server_side)
|
||||||
const deletedIcon = ref(false)
|
const deletedIcon = ref(false)
|
||||||
const visibility = ref(
|
const visibility = ref(
|
||||||
tags.value.approvedStatuses.includes(props.project.status)
|
tags.value.approvedStatuses.includes(project.value.status)
|
||||||
? props.project.status
|
? project.value.status
|
||||||
: props.project.requested_status,
|
: project.value.requested_status,
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasPermission = computed(() => {
|
const hasPermission = computed(() => {
|
||||||
const EDIT_DETAILS = 1 << 2
|
const EDIT_DETAILS = 1 << 2
|
||||||
return (props.currentMember?.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
return ((currentMember.value?.permissions ?? 0) & EDIT_DETAILS) === EDIT_DETAILS
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasDeletePermission = computed(() => {
|
const hasDeletePermission = computed(() => {
|
||||||
const DELETE_PROJECT = 1 << 7
|
const DELETE_PROJECT = 1 << 7
|
||||||
return (props.currentMember?.permissions & DELETE_PROJECT) === DELETE_PROJECT
|
return ((currentMember.value?.permissions ?? 0) & DELETE_PROJECT) === DELETE_PROJECT
|
||||||
})
|
})
|
||||||
|
|
||||||
const summaryWarning = computed(() => {
|
const summaryWarning = computed(() => {
|
||||||
@@ -343,26 +322,26 @@ const sideTypes = ['required', 'optional', 'unsupported']
|
|||||||
const patchData = computed(() => {
|
const patchData = computed(() => {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|
||||||
if (name.value !== props.project.title) {
|
if (name.value !== project.value.title) {
|
||||||
data.title = name.value.trim()
|
data.title = name.value.trim()
|
||||||
}
|
}
|
||||||
if (slug.value !== props.project.slug) {
|
if (slug.value !== project.value.slug) {
|
||||||
data.slug = slug.value.trim()
|
data.slug = slug.value.trim()
|
||||||
}
|
}
|
||||||
if (summary.value !== props.project.description) {
|
if (summary.value !== project.value.description) {
|
||||||
data.description = summary.value.trim()
|
data.description = summary.value.trim()
|
||||||
}
|
}
|
||||||
if (clientSide.value !== props.project.client_side) {
|
if (clientSide.value !== project.value.client_side) {
|
||||||
data.client_side = clientSide.value
|
data.client_side = clientSide.value
|
||||||
}
|
}
|
||||||
if (serverSide.value !== props.project.server_side) {
|
if (serverSide.value !== project.value.server_side) {
|
||||||
data.server_side = serverSide.value
|
data.server_side = serverSide.value
|
||||||
}
|
}
|
||||||
if (tags.value.approvedStatuses.includes(props.project.status)) {
|
if (tags.value.approvedStatuses.includes(project.value.status)) {
|
||||||
if (visibility.value !== props.project.status) {
|
if (visibility.value !== project.value.status) {
|
||||||
data.status = visibility.value
|
data.status = visibility.value
|
||||||
}
|
}
|
||||||
} else if (visibility.value !== props.project.requested_status) {
|
} else if (visibility.value !== project.value.requested_status) {
|
||||||
data.requested_status = visibility.value
|
data.requested_status = visibility.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,23 +353,23 @@ const hasChanges = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hasModifiedVisibility = () => {
|
const hasModifiedVisibility = () => {
|
||||||
const originalVisibility = tags.value.approvedStatuses.includes(props.project.status)
|
const originalVisibility = tags.value.approvedStatuses.includes(project.value.status)
|
||||||
? props.project.status
|
? project.value.status
|
||||||
: props.project.requested_status
|
: project.value.requested_status
|
||||||
|
|
||||||
return originalVisibility !== visibility.value
|
return originalVisibility !== visibility.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveChanges = async () => {
|
const saveChanges = async () => {
|
||||||
if (hasChanges.value) {
|
if (hasChanges.value) {
|
||||||
await props.patchProject(patchData.value)
|
await patchProject(patchData.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deletedIcon.value) {
|
if (deletedIcon.value) {
|
||||||
await deleteIcon()
|
await deleteIcon()
|
||||||
deletedIcon.value = false
|
deletedIcon.value = false
|
||||||
} else if (icon.value) {
|
} else if (icon.value) {
|
||||||
await props.patchIcon(icon.value)
|
await patchIcon(icon.value)
|
||||||
icon.value = null
|
icon.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,12 +380,12 @@ const showPreviewImage = (files) => {
|
|||||||
deletedIcon.value = false
|
deletedIcon.value = false
|
||||||
reader.readAsDataURL(icon.value)
|
reader.readAsDataURL(icon.value)
|
||||||
reader.onload = (event) => {
|
reader.onload = (event) => {
|
||||||
previewImage.value = event.target.result
|
previewImage.value = event.target?.result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteProject = async () => {
|
const deleteProject = async () => {
|
||||||
await useBaseFetch(`project/${props.project.id}`, {
|
await useBaseFetch(`project/${project.value.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
await initUserProjects()
|
await initUserProjects()
|
||||||
@@ -425,10 +404,10 @@ const markIconForDeletion = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteIcon = async () => {
|
const deleteIcon = async () => {
|
||||||
await useBaseFetch(`project/${props.project.id}/icon`, {
|
await useBaseFetch(`project/${project.value.id}/icon`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
await props.resetProject()
|
await refreshProject()
|
||||||
addNotification({
|
addNotification({
|
||||||
title: 'Project icon removed',
|
title: 'Project icon removed',
|
||||||
text: "Your project's icon has been removed.",
|
text: "Your project's icon has been removed.",
|
||||||
|
|||||||
@@ -155,24 +155,18 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SaveIcon } from '@modrinth/assets'
|
import { SaveIcon } from '@modrinth/assets'
|
||||||
import { Checkbox, DropdownSelect } from '@modrinth/ui'
|
import { Checkbox, DropdownSelect, injectProjectPageContext } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
type BuiltinLicense,
|
type BuiltinLicense,
|
||||||
builtinLicenses,
|
builtinLicenses,
|
||||||
formatProjectType,
|
formatProjectType,
|
||||||
type Project,
|
|
||||||
type TeamMember,
|
|
||||||
TeamMemberPermission,
|
TeamMemberPermission,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import { computed, type Ref, ref } from 'vue'
|
import { computed, type Ref, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
|
||||||
project: Project
|
|
||||||
currentMember: TeamMember | undefined
|
|
||||||
patchProject: (payload: object, quiet?: boolean) => object
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const licenseUrl = ref(props.project.license.url)
|
const licenseUrl = ref(project.value.license.url)
|
||||||
const license: Ref<{
|
const license: Ref<{
|
||||||
friendly: string
|
friendly: string
|
||||||
short: string
|
short: string
|
||||||
@@ -183,10 +177,10 @@ const license: Ref<{
|
|||||||
requiresOnlyOrLater: false,
|
requiresOnlyOrLater: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const allowOrLater = ref(props.project.license.id.includes('-or-later'))
|
const allowOrLater = ref(project.value.license.id.includes('-or-later'))
|
||||||
const nonSpdxLicense = ref(props.project.license.id.includes('LicenseRef-'))
|
const nonSpdxLicense = ref(project.value.license.id.includes('LicenseRef-'))
|
||||||
|
|
||||||
const oldLicenseId = props.project.license.id
|
const oldLicenseId = project.value.license.id
|
||||||
const trimmedLicenseId = oldLicenseId
|
const trimmedLicenseId = oldLicenseId
|
||||||
.replaceAll('-only', '')
|
.replaceAll('-only', '')
|
||||||
.replaceAll('-or-later', '')
|
.replaceAll('-or-later', '')
|
||||||
@@ -208,7 +202,7 @@ if (oldLicenseId === 'LicenseRef-Unknown') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasPermission = computed(() => {
|
const hasPermission = computed(() => {
|
||||||
return (props.currentMember?.permissions ?? 0) & TeamMemberPermission.EDIT_DETAILS
|
return (currentMember.value?.permissions ?? 0) & TeamMemberPermission.EDIT_DETAILS
|
||||||
})
|
})
|
||||||
|
|
||||||
const licenseId = computed(() => {
|
const licenseId = computed(() => {
|
||||||
@@ -240,11 +234,11 @@ const patchRequestPayload = computed(() => {
|
|||||||
license_url?: string | null // null = remove url
|
license_url?: string | null // null = remove url
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (licenseId.value !== props.project.license.id) {
|
if (licenseId.value !== project.value.license.id) {
|
||||||
payload.license_id = licenseId.value
|
payload.license_id = licenseId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
if (licenseUrl.value !== props.project.license.url) {
|
if (licenseUrl.value !== project.value.license.url) {
|
||||||
payload.license_url = licenseUrl.value ? licenseUrl.value : null
|
payload.license_url = licenseUrl.value ? licenseUrl.value : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +250,6 @@ const hasChanges = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function saveChanges() {
|
function saveChanges() {
|
||||||
props.patchProject(patchRequestPayload.value)
|
patchProject(patchRequestPayload.value)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -174,35 +174,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { SaveIcon, TriangleAlertIcon } from '@modrinth/assets'
|
import { SaveIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||||
import { commonLinkDomains, isCommonUrl, isDiscordUrl, isLinkShortener } from '@modrinth/moderation'
|
import { commonLinkDomains, isCommonUrl, isDiscordUrl, isLinkShortener } from '@modrinth/moderation'
|
||||||
import { DropdownSelect } from '@modrinth/ui'
|
import { DropdownSelect, injectProjectPageContext } from '@modrinth/ui'
|
||||||
|
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
|
|
||||||
const props = defineProps({
|
const { projectV2: project, currentMember, patchProject } = injectProjectPageContext()
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
patchProject: {
|
|
||||||
type: Function,
|
|
||||||
default() {
|
|
||||||
return () => {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const issuesUrl = ref(props.project.issues_url)
|
const issuesUrl = ref(project.value.issues_url)
|
||||||
const sourceUrl = ref(props.project.source_url)
|
const sourceUrl = ref(project.value.source_url)
|
||||||
const wikiUrl = ref(props.project.wiki_url)
|
const wikiUrl = ref(project.value.wiki_url)
|
||||||
const discordUrl = ref(props.project.discord_url)
|
const discordUrl = ref(project.value.discord_url)
|
||||||
|
|
||||||
const isIssuesUrlCommon = computed(() => {
|
const isIssuesUrlCommon = computed(() => {
|
||||||
if (!issuesUrl.value || issuesUrl.value.trim().length === 0) return true
|
if (!issuesUrl.value || issuesUrl.value.trim().length === 0) return true
|
||||||
@@ -244,7 +225,7 @@ const isDiscordLinkShortener = computed(() => {
|
|||||||
return isLinkShortener(discordUrl.value)
|
return isLinkShortener(discordUrl.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const rawDonationLinks = JSON.parse(JSON.stringify(props.project.donation_urls))
|
const rawDonationLinks = JSON.parse(JSON.stringify(project.value.donation_urls))
|
||||||
rawDonationLinks.push({
|
rawDonationLinks.push({
|
||||||
id: null,
|
id: null,
|
||||||
platform: null,
|
platform: null,
|
||||||
@@ -254,32 +235,32 @@ const donationLinks = ref(rawDonationLinks)
|
|||||||
|
|
||||||
const hasPermission = computed(() => {
|
const hasPermission = computed(() => {
|
||||||
const EDIT_DETAILS = 1 << 2
|
const EDIT_DETAILS = 1 << 2
|
||||||
return (props.currentMember?.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
return (currentMember.value?.permissions & EDIT_DETAILS) === EDIT_DETAILS
|
||||||
})
|
})
|
||||||
|
|
||||||
const patchData = computed(() => {
|
const patchData = computed(() => {
|
||||||
const data = {}
|
const data = {}
|
||||||
|
|
||||||
if (checkDifference(issuesUrl.value, props.project.issues_url)) {
|
if (checkDifference(issuesUrl.value, project.value.issues_url)) {
|
||||||
data.issues_url = issuesUrl.value === '' ? null : issuesUrl.value.trim()
|
data.issues_url = issuesUrl.value === '' ? null : issuesUrl.value.trim()
|
||||||
}
|
}
|
||||||
if (checkDifference(sourceUrl.value, props.project.source_url)) {
|
if (checkDifference(sourceUrl.value, project.value.source_url)) {
|
||||||
data.source_url = sourceUrl.value === '' ? null : sourceUrl.value.trim()
|
data.source_url = sourceUrl.value === '' ? null : sourceUrl.value.trim()
|
||||||
}
|
}
|
||||||
if (checkDifference(wikiUrl.value, props.project.wiki_url)) {
|
if (checkDifference(wikiUrl.value, project.value.wiki_url)) {
|
||||||
data.wiki_url = wikiUrl.value === '' ? null : wikiUrl.value.trim()
|
data.wiki_url = wikiUrl.value === '' ? null : wikiUrl.value.trim()
|
||||||
}
|
}
|
||||||
if (checkDifference(discordUrl.value, props.project.discord_url)) {
|
if (checkDifference(discordUrl.value, project.value.discord_url)) {
|
||||||
data.discord_url = discordUrl.value === '' ? null : discordUrl.value.trim()
|
data.discord_url = discordUrl.value === '' ? null : discordUrl.value.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
const validDonationLinks = donationLinks.value.filter((link) => link.url && link.id)
|
const validDonationLinks = donationLinks.value.filter((link) => link.url && link.id)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
validDonationLinks !== props.project.donation_urls &&
|
validDonationLinks !== project.value.donation_urls &&
|
||||||
!(
|
!(
|
||||||
props.project.donation_urls &&
|
project.value.donation_urls &&
|
||||||
props.project.donation_urls.length === 0 &&
|
project.value.donation_urls.length === 0 &&
|
||||||
validDonationLinks.length === 0
|
validDonationLinks.length === 0
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -301,8 +282,8 @@ const hasChanges = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
async function saveChanges() {
|
async function saveChanges() {
|
||||||
if (patchData.value && (await props.patchProject(patchData.value))) {
|
if (patchData.value && (await patchProject(patchData.value))) {
|
||||||
donationLinks.value = JSON.parse(JSON.stringify(props.project.donation_urls))
|
donationLinks.value = JSON.parse(JSON.stringify(project.value.donation_urls))
|
||||||
donationLinks.value.push({
|
donationLinks.value.push({
|
||||||
id: null,
|
id: null,
|
||||||
platform: null,
|
platform: null,
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
v-model="currentUsername"
|
v-model="currentUsername"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
:disabled="(props.currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
|
:disabled="(currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
|
||||||
@keypress.enter="inviteTeamMember()"
|
@keypress.enter="inviteTeamMember()"
|
||||||
/>
|
/>
|
||||||
<label for="username" class="hidden">Username</label>
|
<label for="username" class="hidden">Username</label>
|
||||||
<button
|
<button
|
||||||
class="iconified-button brand-button"
|
class="iconified-button brand-button"
|
||||||
:disabled="(props.currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
|
:disabled="(currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES"
|
||||||
@click="inviteTeamMember()"
|
@click="inviteTeamMember()"
|
||||||
>
|
>
|
||||||
<UserPlusIcon />
|
<UserPlusIcon />
|
||||||
@@ -47,11 +47,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
class="iconified-button danger-button"
|
class="iconified-button danger-button"
|
||||||
:disabled="props.currentMember?.is_owner"
|
:disabled="currentMember?.is_owner"
|
||||||
:title="
|
:title="
|
||||||
props.currentMember?.is_owner
|
currentMember?.is_owner ? 'You cannot leave the project if you are the owner!' : ''
|
||||||
? 'You cannot leave the project if you are the owner!'
|
|
||||||
: ''
|
|
||||||
"
|
"
|
||||||
@click="leaveProject()"
|
@click="leaveProject()"
|
||||||
>
|
>
|
||||||
@@ -104,7 +102,7 @@
|
|||||||
:id="`member-${allTeamMembers[index].user.username}-role`"
|
:id="`member-${allTeamMembers[index].user.username}-role`"
|
||||||
v-model="allTeamMembers[index].role"
|
v-model="allTeamMembers[index].role"
|
||||||
type="text"
|
type="text"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="adjacent-input">
|
<div class="adjacent-input">
|
||||||
@@ -119,7 +117,7 @@
|
|||||||
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
:id="`member-${allTeamMembers[index].user.username}-monetization-weight`"
|
||||||
v-model="allTeamMembers[index].payouts_split"
|
v-model="allTeamMembers[index].payouts_split"
|
||||||
type="number"
|
type="number"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!member.is_owner">
|
<template v-if="!member.is_owner">
|
||||||
@@ -130,8 +128,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
:model-value="(member?.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
(currentMember?.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION
|
||||||
"
|
"
|
||||||
label="Upload version"
|
label="Upload version"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
@update:model-value="allTeamMembers[index].permissions ^= UPLOAD_VERSION"
|
||||||
@@ -139,8 +137,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & DELETE_VERSION) === DELETE_VERSION"
|
:model-value="(member?.permissions & DELETE_VERSION) === DELETE_VERSION"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & DELETE_VERSION) !== DELETE_VERSION
|
(currentMember?.permissions & DELETE_VERSION) !== DELETE_VERSION
|
||||||
"
|
"
|
||||||
label="Delete version"
|
label="Delete version"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
@update:model-value="allTeamMembers[index].permissions ^= DELETE_VERSION"
|
||||||
@@ -148,8 +146,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
:model-value="(member?.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
(currentMember?.permissions & EDIT_DETAILS) !== EDIT_DETAILS
|
||||||
"
|
"
|
||||||
label="Edit details"
|
label="Edit details"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
@update:model-value="allTeamMembers[index].permissions ^= EDIT_DETAILS"
|
||||||
@@ -157,8 +155,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & EDIT_BODY) === EDIT_BODY"
|
:model-value="(member?.permissions & EDIT_BODY) === EDIT_BODY"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & EDIT_BODY) !== EDIT_BODY
|
(currentMember?.permissions & EDIT_BODY) !== EDIT_BODY
|
||||||
"
|
"
|
||||||
label="Edit body"
|
label="Edit body"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= EDIT_BODY"
|
@update:model-value="allTeamMembers[index].permissions ^= EDIT_BODY"
|
||||||
@@ -166,8 +164,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
:model-value="(member?.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
(currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES
|
||||||
"
|
"
|
||||||
label="Manage invites"
|
label="Manage invites"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
@update:model-value="allTeamMembers[index].permissions ^= MANAGE_INVITES"
|
||||||
@@ -175,23 +173,23 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
:model-value="(member?.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
(currentMember?.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER
|
||||||
"
|
"
|
||||||
label="Remove member"
|
label="Remove member"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
@update:model-value="allTeamMembers[index].permissions ^= REMOVE_MEMBER"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
:model-value="(member?.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
label="Edit member"
|
label="Edit member"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
@update:model-value="allTeamMembers[index].permissions ^= EDIT_MEMBER"
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
:model-value="(member?.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
(currentMember?.permissions & DELETE_PROJECT) !== DELETE_PROJECT
|
||||||
"
|
"
|
||||||
label="Delete project"
|
label="Delete project"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
@update:model-value="allTeamMembers[index].permissions ^= DELETE_PROJECT"
|
||||||
@@ -199,8 +197,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
:model-value="(member?.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS
|
(currentMember?.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS
|
||||||
"
|
"
|
||||||
label="View analytics"
|
label="View analytics"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
|
@update:model-value="allTeamMembers[index].permissions ^= VIEW_ANALYTICS"
|
||||||
@@ -208,8 +206,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
:model-value="(member?.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS
|
(currentMember?.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS
|
||||||
"
|
"
|
||||||
label="View revenue"
|
label="View revenue"
|
||||||
@update:model-value="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
|
@update:model-value="allTeamMembers[index].permissions ^= VIEW_PAYOUTS"
|
||||||
@@ -219,7 +217,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button
|
<button
|
||||||
class="iconified-button brand-button"
|
class="iconified-button brand-button"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
@click="updateTeamMember(index)"
|
@click="updateTeamMember(index)"
|
||||||
>
|
>
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
@@ -228,14 +226,14 @@
|
|||||||
<button
|
<button
|
||||||
v-if="!member.is_owner"
|
v-if="!member.is_owner"
|
||||||
class="iconified-button danger-button"
|
class="iconified-button danger-button"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
@click="removeTeamMember(index)"
|
@click="removeTeamMember(index)"
|
||||||
>
|
>
|
||||||
<UserXIcon />
|
<UserXIcon />
|
||||||
Remove member
|
Remove member
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="!member.is_owner && props.currentMember?.is_owner && member.accepted"
|
v-if="!member.is_owner && currentMember?.is_owner && member.accepted"
|
||||||
class="iconified-button"
|
class="iconified-button"
|
||||||
@click="transferOwnership(index)"
|
@click="transferOwnership(index)"
|
||||||
>
|
>
|
||||||
@@ -249,26 +247,26 @@
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label__title size-card-header">Organization</span>
|
<span class="label__title size-card-header">Organization</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.organization">
|
<div v-if="organization">
|
||||||
<p>
|
<p>
|
||||||
This project is managed by {{ props.organization.name }}. The defaults for member
|
This project is managed by {{ organization.name }}. The defaults for member permissions
|
||||||
permissions are set in the
|
are set in the
|
||||||
<nuxt-link :to="`/organization/${props.organization.slug}/settings/members`">
|
<nuxt-link :to="`/organization/${organization.slug}/settings/members`">
|
||||||
organization settings
|
organization settings
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
. You may override them below.
|
. You may override them below.
|
||||||
</p>
|
</p>
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
:to="`/organization/${props.organization.slug}`"
|
:to="`/organization/${organization.slug}`"
|
||||||
class="universal-card button-base recessed org"
|
class="universal-card button-base recessed org"
|
||||||
>
|
>
|
||||||
<Avatar :src="props.organization.icon_url" :alt="props.organization.name" size="md" />
|
<Avatar :src="organization.icon_url" :alt="organization.name" size="md" />
|
||||||
<div class="details">
|
<div class="details">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
{{ props.organization.name }}
|
{{ organization.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
{{ props.organization.description }}
|
{{ organization.description }}
|
||||||
</div>
|
</div>
|
||||||
<span class="stat-bar">
|
<span class="stat-bar">
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
@@ -288,7 +286,7 @@
|
|||||||
This project is not managed by an organization. If you are the member of any organizations,
|
This project is not managed by an organization. If you are the member of any organizations,
|
||||||
you can transfer management to one of them.
|
you can transfer management to one of them.
|
||||||
</p>
|
</p>
|
||||||
<div v-if="!props.organization" class="input-group">
|
<div v-if="!organization" class="input-group">
|
||||||
<Multiselect
|
<Multiselect
|
||||||
id="organization-picker"
|
id="organization-picker"
|
||||||
v-model="selectedOrganization"
|
v-model="selectedOrganization"
|
||||||
@@ -300,14 +298,14 @@
|
|||||||
:show-labels="false"
|
:show-labels="false"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
:options="organizations || []"
|
:options="organizations || []"
|
||||||
:disabled="!props.currentMember?.is_owner || organizations?.length === 0"
|
:disabled="!currentMember?.is_owner || organizations?.length === 0"
|
||||||
/>
|
/>
|
||||||
<button class="btn btn-primary" :disabled="!selectedOrganization" @click="onAddToOrg">
|
<button class="btn btn-primary" :disabled="!selectedOrganization" @click="onAddToOrg">
|
||||||
<CheckIcon />
|
<CheckIcon />
|
||||||
Transfer management
|
Transfer management
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button v-if="props.organization" class="btn" @click="$refs.modal_remove.show()">
|
<button v-if="organization" class="btn" @click="$refs.modal_remove.show()">
|
||||||
<OrganizationIcon />
|
<OrganizationIcon />
|
||||||
Remove from organization
|
Remove from organization
|
||||||
</button>
|
</button>
|
||||||
@@ -358,7 +356,7 @@
|
|||||||
v-model="allOrgMembers[index].override"
|
v-model="allOrgMembers[index].override"
|
||||||
class="switch stylized-toggle"
|
class="switch stylized-toggle"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="adjacent-input">
|
<div class="adjacent-input">
|
||||||
@@ -373,7 +371,7 @@
|
|||||||
v-model="allOrgMembers[index].role"
|
v-model="allOrgMembers[index].role"
|
||||||
type="text"
|
type="text"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -391,7 +389,7 @@
|
|||||||
v-model="allOrgMembers[index].payouts_split"
|
v-model="allOrgMembers[index].payouts_split"
|
||||||
type="number"
|
type="number"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -404,8 +402,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
:model-value="(member?.permissions & UPLOAD_VERSION) === UPLOAD_VERSION"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION ||
|
(currentMember?.permissions & UPLOAD_VERSION) !== UPLOAD_VERSION ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Upload version"
|
label="Upload version"
|
||||||
@@ -414,8 +412,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & DELETE_VERSION) === DELETE_VERSION"
|
:model-value="(member?.permissions & DELETE_VERSION) === DELETE_VERSION"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & DELETE_VERSION) !== DELETE_VERSION ||
|
(currentMember?.permissions & DELETE_VERSION) !== DELETE_VERSION ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Delete version"
|
label="Delete version"
|
||||||
@@ -424,8 +422,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
:model-value="(member?.permissions & EDIT_DETAILS) === EDIT_DETAILS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & EDIT_DETAILS) !== EDIT_DETAILS ||
|
(currentMember?.permissions & EDIT_DETAILS) !== EDIT_DETAILS ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Edit details"
|
label="Edit details"
|
||||||
@@ -434,8 +432,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & EDIT_BODY) === EDIT_BODY"
|
:model-value="(member?.permissions & EDIT_BODY) === EDIT_BODY"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & EDIT_BODY) !== EDIT_BODY ||
|
(currentMember?.permissions & EDIT_BODY) !== EDIT_BODY ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Edit body"
|
label="Edit body"
|
||||||
@@ -444,8 +442,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
:model-value="(member?.permissions & MANAGE_INVITES) === MANAGE_INVITES"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES ||
|
(currentMember?.permissions & MANAGE_INVITES) !== MANAGE_INVITES ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Manage invites"
|
label="Manage invites"
|
||||||
@@ -454,8 +452,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
:model-value="(member?.permissions & REMOVE_MEMBER) === REMOVE_MEMBER"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER ||
|
(currentMember?.permissions & REMOVE_MEMBER) !== REMOVE_MEMBER ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Remove member"
|
label="Remove member"
|
||||||
@@ -464,7 +462,7 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
:model-value="(member?.permissions & EDIT_MEMBER) === EDIT_MEMBER"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Edit member"
|
label="Edit member"
|
||||||
@@ -473,8 +471,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
:model-value="(member?.permissions & DELETE_PROJECT) === DELETE_PROJECT"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & DELETE_PROJECT) !== DELETE_PROJECT ||
|
(currentMember?.permissions & DELETE_PROJECT) !== DELETE_PROJECT ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="Delete project"
|
label="Delete project"
|
||||||
@@ -483,8 +481,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
:model-value="(member?.permissions & VIEW_ANALYTICS) === VIEW_ANALYTICS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS ||
|
(currentMember?.permissions & VIEW_ANALYTICS) !== VIEW_ANALYTICS ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="View analytics"
|
label="View analytics"
|
||||||
@@ -493,8 +491,8 @@
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
:model-value="(member?.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
:model-value="(member?.permissions & VIEW_PAYOUTS) === VIEW_PAYOUTS"
|
||||||
:disabled="
|
:disabled="
|
||||||
(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER ||
|
||||||
(props.currentMember?.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS ||
|
(currentMember?.permissions & VIEW_PAYOUTS) !== VIEW_PAYOUTS ||
|
||||||
!allOrgMembers[index].override
|
!allOrgMembers[index].override
|
||||||
"
|
"
|
||||||
label="View revenue"
|
label="View revenue"
|
||||||
@@ -505,7 +503,7 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<button
|
<button
|
||||||
class="iconified-button brand-button"
|
class="iconified-button brand-button"
|
||||||
:disabled="(props.currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
:disabled="(currentMember?.permissions & EDIT_MEMBER) !== EDIT_MEMBER"
|
||||||
@click="updateOrgMember(index)"
|
@click="updateOrgMember(index)"
|
||||||
>
|
>
|
||||||
<SaveIcon />
|
<SaveIcon />
|
||||||
@@ -536,54 +534,22 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
|
injectProjectPageContext,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { Multiselect } from 'vue-multiselect'
|
import { Multiselect } from 'vue-multiselect'
|
||||||
|
|
||||||
import { removeSelfFromTeam } from '~/helpers/teams.js'
|
import { removeSelfFromTeam } from '~/helpers/teams.js'
|
||||||
|
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
|
const {
|
||||||
const props = defineProps({
|
projectV2: project,
|
||||||
project: {
|
organization,
|
||||||
type: Object,
|
allMembers,
|
||||||
default() {
|
currentMember,
|
||||||
return {}
|
refreshProject,
|
||||||
},
|
refreshOrganization,
|
||||||
},
|
refreshMembers,
|
||||||
organization: {
|
} = injectProjectPageContext()
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
allMembers: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
resetProject: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
resetOrganization: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
resetMembers: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
default: () => {},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const cosmetics = useCosmetics()
|
const cosmetics = useCosmetics()
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
@@ -592,14 +558,14 @@ const allTeamMembers = ref([])
|
|||||||
const allOrgMembers = ref([])
|
const allOrgMembers = ref([])
|
||||||
|
|
||||||
const acceptedOrgMembers = computed(() => {
|
const acceptedOrgMembers = computed(() => {
|
||||||
return props.organization?.members?.filter((x) => x.accepted) || []
|
return organization.value?.members?.filter((x) => x.accepted) || []
|
||||||
})
|
})
|
||||||
|
|
||||||
function initMembers() {
|
function initMembers() {
|
||||||
const orgMembers = props.organization?.members || []
|
const orgMembers = organization.value?.members || []
|
||||||
|
|
||||||
const selectedMembersForOrg = orgMembers.map((partialOrgMember) => {
|
const selectedMembersForOrg = orgMembers.map((partialOrgMember) => {
|
||||||
const foundMember = props.allMembers.find((tM) => tM.user.id === partialOrgMember.user.id)
|
const foundMember = allMembers.value.find((tM) => tM.user.id === partialOrgMember.user.id)
|
||||||
const returnVal = foundMember ?? partialOrgMember
|
const returnVal = foundMember ?? partialOrgMember
|
||||||
|
|
||||||
// If replacing a partial with a full member, we need to mark as such.
|
// If replacing a partial with a full member, we need to mark as such.
|
||||||
@@ -613,20 +579,12 @@ function initMembers() {
|
|||||||
|
|
||||||
allOrgMembers.value = selectedMembersForOrg
|
allOrgMembers.value = selectedMembersForOrg
|
||||||
|
|
||||||
allTeamMembers.value = props.allMembers.filter(
|
allTeamMembers.value = allMembers.value.filter(
|
||||||
(x) => !selectedMembersForOrg.some((y) => y.user.id === x.user.id),
|
(x) => !selectedMembersForOrg.some((y) => y.user.id === x.user.id),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch([allMembers, organization, project, currentMember], initMembers)
|
||||||
[
|
|
||||||
() => props.allMembers,
|
|
||||||
() => props.organization,
|
|
||||||
() => props.project,
|
|
||||||
() => props.currentMember,
|
|
||||||
],
|
|
||||||
initMembers,
|
|
||||||
)
|
|
||||||
initMembers()
|
initMembers()
|
||||||
|
|
||||||
const currentUsername = ref('')
|
const currentUsername = ref('')
|
||||||
@@ -656,7 +614,7 @@ const onAddToOrg = useClientTry(async () => {
|
|||||||
await useBaseFetch(`organization/${selectedOrganization.value.id}/projects`, {
|
await useBaseFetch(`organization/${selectedOrganization.value.id}/projects`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
project_id: props.project.id,
|
project_id: project.value.id,
|
||||||
}),
|
}),
|
||||||
apiVersion: 3,
|
apiVersion: 3,
|
||||||
})
|
})
|
||||||
@@ -671,9 +629,9 @@ const onAddToOrg = useClientTry(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const onRemoveFromOrg = useClientTry(async () => {
|
const onRemoveFromOrg = useClientTry(async () => {
|
||||||
if (!props.project.organization || !auth.value?.user?.id) return
|
if (!project.value.organization || !auth.value?.user?.id) return
|
||||||
|
|
||||||
await useBaseFetch(`organization/${props.project.organization}/projects/${props.project.id}`, {
|
await useBaseFetch(`organization/${project.value.organization}/projects/${project.value.id}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
new_owner: auth.value.user.id,
|
new_owner: auth.value.user.id,
|
||||||
@@ -691,7 +649,7 @@ const onRemoveFromOrg = useClientTry(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const leaveProject = async () => {
|
const leaveProject = async () => {
|
||||||
await removeSelfFromTeam(props.project.team)
|
await removeSelfFromTeam(project.value.team)
|
||||||
navigateTo('/dashboard/projects')
|
navigateTo('/dashboard/projects')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,7 +661,7 @@ const inviteTeamMember = async () => {
|
|||||||
const data = {
|
const data = {
|
||||||
user_id: user.id.trim(),
|
user_id: user.id.trim(),
|
||||||
}
|
}
|
||||||
await useBaseFetch(`team/${props.project.team}/members`, {
|
await useBaseFetch(`team/${project.value.team}/members`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data,
|
body: data,
|
||||||
})
|
})
|
||||||
@@ -725,7 +683,7 @@ const removeTeamMember = async (index) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await useBaseFetch(
|
await useBaseFetch(
|
||||||
`team/${props.project.team}/members/${allTeamMembers.value[index].user.id}`,
|
`team/${project.value.team}/members/${allTeamMembers.value[index].user.id}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
@@ -758,7 +716,7 @@ const updateTeamMember = async (index) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await useBaseFetch(
|
await useBaseFetch(
|
||||||
`team/${props.project.team}/members/${allTeamMembers.value[index].user.id}`,
|
`team/${project.value.team}/members/${allTeamMembers.value[index].user.id}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: data,
|
body: data,
|
||||||
@@ -785,7 +743,7 @@ const transferOwnership = async (index) => {
|
|||||||
startLoading()
|
startLoading()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await useBaseFetch(`team/${props.project.team}/owner`, {
|
await useBaseFetch(`team/${project.value.team}/owner`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: {
|
||||||
user_id: allTeamMembers.value[index].user.id,
|
user_id: allTeamMembers.value[index].user.id,
|
||||||
@@ -808,7 +766,7 @@ async function updateOrgMember(index) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (allOrgMembers.value[index].override && !allOrgMembers.value[index].oldOverride) {
|
if (allOrgMembers.value[index].override && !allOrgMembers.value[index].oldOverride) {
|
||||||
await useBaseFetch(`team/${props.project.team}/members`, {
|
await useBaseFetch(`team/${project.value.team}/members`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
permissions: allOrgMembers.value[index].permissions,
|
permissions: allOrgMembers.value[index].permissions,
|
||||||
@@ -819,14 +777,14 @@ async function updateOrgMember(index) {
|
|||||||
})
|
})
|
||||||
} else if (!allOrgMembers.value[index].override && allOrgMembers.value[index].oldOverride) {
|
} else if (!allOrgMembers.value[index].override && allOrgMembers.value[index].oldOverride) {
|
||||||
await useBaseFetch(
|
await useBaseFetch(
|
||||||
`team/${props.project.team}/members/${allOrgMembers.value[index].user.id}`,
|
`team/${project.value.team}/members/${allOrgMembers.value[index].user.id}`,
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
await useBaseFetch(
|
await useBaseFetch(
|
||||||
`team/${props.project.team}/members/${allOrgMembers.value[index].user.id}`,
|
`team/${project.value.team}/members/${allOrgMembers.value[index].user.id}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: {
|
body: {
|
||||||
@@ -850,7 +808,7 @@ async function updateOrgMember(index) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateMembers = async () => {
|
const updateMembers = async () => {
|
||||||
await Promise.all([props.resetProject(), props.resetOrganization(), props.resetMembers()])
|
await Promise.all([refreshProject(), refreshOrganization(), refreshMembers()])
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -134,12 +134,11 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { SaveIcon, StarIcon, TriangleAlertIcon } from '@modrinth/assets'
|
import { SaveIcon, StarIcon, TriangleAlertIcon } from '@modrinth/assets'
|
||||||
import { Checkbox } from '@modrinth/ui'
|
import { Checkbox, injectProjectPageContext } from '@modrinth/ui'
|
||||||
import {
|
import {
|
||||||
formatCategory,
|
formatCategory,
|
||||||
formatCategoryHeader,
|
formatCategoryHeader,
|
||||||
formatProjectType,
|
formatProjectType,
|
||||||
type Project,
|
|
||||||
sortedCategories,
|
sortedCategories,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
@@ -151,50 +150,31 @@ interface Category {
|
|||||||
project_type: string
|
project_type: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
|
||||||
project: Project & {
|
|
||||||
actualProjectType: string
|
|
||||||
}
|
|
||||||
allMembers?: any[]
|
|
||||||
currentMember?: any
|
|
||||||
patchProject?: (data: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const { projectV2: project, patchProject } = injectProjectPageContext()
|
||||||
allMembers: () => [],
|
|
||||||
currentMember: null,
|
|
||||||
patchProject: () => {
|
|
||||||
addNotification({
|
|
||||||
title: 'An error occurred',
|
|
||||||
text: 'Patch project function not found',
|
|
||||||
type: 'error',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const selectedTags = ref<Category[]>(
|
const selectedTags = ref<Category[]>(
|
||||||
sortedCategories(tags.value).filter(
|
sortedCategories(tags.value).filter(
|
||||||
(x: Category) =>
|
(x: Category) =>
|
||||||
x.project_type === props.project.actualProjectType &&
|
x.project_type === project.value.actualProjectType &&
|
||||||
(props.project.categories.includes(x.name) ||
|
(project.value.categories.includes(x.name) ||
|
||||||
props.project.additional_categories.includes(x.name)),
|
project.value.additional_categories.includes(x.name)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const featuredTags = ref<Category[]>(
|
const featuredTags = ref<Category[]>(
|
||||||
sortedCategories(tags.value).filter(
|
sortedCategories(tags.value).filter(
|
||||||
(x: Category) =>
|
(x: Category) =>
|
||||||
x.project_type === props.project.actualProjectType &&
|
x.project_type === project.value.actualProjectType &&
|
||||||
props.project.categories.includes(x.name),
|
project.value.categories.includes(x.name),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const categoryLists = computed(() => {
|
const categoryLists = computed(() => {
|
||||||
const lists: Record<string, Category[]> = {}
|
const lists: Record<string, Category[]> = {}
|
||||||
sortedCategories(tags.value).forEach((x: Category) => {
|
sortedCategories(tags.value).forEach((x: Category) => {
|
||||||
if (x.project_type === props.project.actualProjectType) {
|
if (x.project_type === project.value.actualProjectType) {
|
||||||
const header = x.header
|
const header = x.header
|
||||||
if (!lists[header]) {
|
if (!lists[header]) {
|
||||||
lists[header] = []
|
lists[header] = []
|
||||||
@@ -214,7 +194,7 @@ const tooManyTagsWarning = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const multipleResolutionTagsWarning = computed(() => {
|
const multipleResolutionTagsWarning = computed(() => {
|
||||||
if (props.project.project_type !== 'resourcepack') return null
|
if (project.value.actualProjectType !== 'resourcepack') return null
|
||||||
|
|
||||||
const resolutionTags = selectedTags.value.filter((tag) =>
|
const resolutionTags = selectedTags.value.filter((tag) =>
|
||||||
['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+'].includes(tag.name),
|
['8x-', '16x', '32x', '48x', '64x', '128x', '256x', '512x+'].includes(tag.name),
|
||||||
@@ -235,7 +215,7 @@ const multipleResolutionTagsWarning = computed(() => {
|
|||||||
|
|
||||||
const allTagsSelectedWarning = computed(() => {
|
const allTagsSelectedWarning = computed(() => {
|
||||||
const categoriesForProjectType = sortedCategories(tags.value).filter(
|
const categoriesForProjectType = sortedCategories(tags.value).filter(
|
||||||
(x: Category) => x.project_type === props.project.actualProjectType,
|
(x: Category) => x.project_type === project.value.actualProjectType,
|
||||||
)
|
)
|
||||||
const totalSelectedTags = selectedTags.value.length
|
const totalSelectedTags = selectedTags.value.length
|
||||||
|
|
||||||
@@ -268,15 +248,15 @@ const patchData = computed(() => {
|
|||||||
.map((x) => x.name)
|
.map((x) => x.name)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
categories.length !== props.project.categories.length ||
|
categories.length !== project.value.categories.length ||
|
||||||
categories.some((value) => !props.project.categories.includes(value))
|
categories.some((value) => !project.value.categories.includes(value))
|
||||||
) {
|
) {
|
||||||
data.categories = categories
|
data.categories = categories
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
additionalCategories.length !== props.project.additional_categories.length ||
|
additionalCategories.length !== project.value.additional_categories.length ||
|
||||||
additionalCategories.some((value) => !props.project.additional_categories.includes(value))
|
additionalCategories.some((value) => !project.value.additional_categories.includes(value))
|
||||||
) {
|
) {
|
||||||
data.additional_categories = additionalCategories
|
data.additional_categories = additionalCategories
|
||||||
}
|
}
|
||||||
@@ -309,7 +289,7 @@ const toggleFeaturedCategory = (category: Category) => {
|
|||||||
|
|
||||||
const saveChanges = () => {
|
const saveChanges = () => {
|
||||||
if (hasChanges.value) {
|
if (hasChanges.value) {
|
||||||
props.patchProject(patchData.value)
|
patchProject(patchData.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ProjectPageVersions
|
<ProjectPageVersions
|
||||||
v-if="versions.length > 0"
|
v-if="versions?.length"
|
||||||
:project="project"
|
:project="project"
|
||||||
:versions="versionsWithDisplayUrl"
|
:versions="versionsWithDisplayUrl"
|
||||||
:show-files="flags.showVersionFilesInTable"
|
:show-files="flags.showVersionFilesInTable"
|
||||||
@@ -207,7 +207,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</ProjectPageVersions>
|
</ProjectPageVersions>
|
||||||
|
|
||||||
<template v-if="!versions.length">
|
<template v-if="!versions?.length">
|
||||||
<div class="grid place-content-center py-10">
|
<div class="grid place-content-center py-10">
|
||||||
<svg
|
<svg
|
||||||
width="250"
|
width="250"
|
||||||
@@ -309,18 +309,9 @@ import { useTemplateRef } from 'vue'
|
|||||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||||
|
|
||||||
interface Props {
|
|
||||||
project: Labrinth.Projects.v2.Project
|
|
||||||
currentMember?: object
|
|
||||||
}
|
|
||||||
|
|
||||||
const { project, currentMember } = defineProps<Props>()
|
|
||||||
|
|
||||||
const versions = defineModel<Labrinth.Versions.v3.Version[]>('versions', { required: true })
|
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { refreshVersions } = injectProjectPageContext()
|
const { projectV2: project, currentMember, versions, refreshVersions } = injectProjectPageContext()
|
||||||
|
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
@@ -331,7 +322,7 @@ const deleteVersionModal = ref<InstanceType<typeof ConfirmModal>>()
|
|||||||
const selectedVersion = ref<string | null>(null)
|
const selectedVersion = ref<string | null>(null)
|
||||||
|
|
||||||
const handleOpenCreateVersionModal = () => {
|
const handleOpenCreateVersionModal = () => {
|
||||||
if (!currentMember) return
|
if (!currentMember.value) return
|
||||||
createProjectVersionModal.value?.openCreateVersionModal()
|
createProjectVersionModal.value?.openCreateVersionModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,12 +331,12 @@ const handleOpenEditVersionModal = (
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
stageId?: string | null,
|
stageId?: string | null,
|
||||||
) => {
|
) => {
|
||||||
if (!currentMember) return
|
if (!currentMember.value) return
|
||||||
createProjectVersionModal.value?.openEditVersionModal(versionId, projectId, stageId)
|
createProjectVersionModal.value?.openEditVersionModal(versionId, projectId, stageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionsWithDisplayUrl = computed(() =>
|
const versionsWithDisplayUrl = computed(() =>
|
||||||
versions.value.map((v) => ({
|
(versions.value ?? []).map((v) => ({
|
||||||
...v,
|
...v,
|
||||||
displayUrlEnding: v.id,
|
displayUrlEnding: v.id,
|
||||||
})),
|
})),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,260 +1,272 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="experimental-styles-within overflow-visible">
|
<section class="experimental-styles-within overflow-visible">
|
||||||
<CreateProjectVersionModal
|
<!-- Loading state -->
|
||||||
v-if="currentMember"
|
<div
|
||||||
ref="create-project-version-modal"
|
v-if="versionsLoading && !versions?.length"
|
||||||
></CreateProjectVersionModal>
|
class="flex items-center justify-center gap-2 py-8"
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
v-if="currentMember"
|
|
||||||
ref="deleteVersionModal"
|
|
||||||
title="Are you sure you want to delete this version?"
|
|
||||||
description="This will remove this version forever (like really forever)."
|
|
||||||
:has-to-type="false"
|
|
||||||
proceed-label="Delete"
|
|
||||||
@proceed="deleteVersion()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Admonition v-if="!hideVersionsAdmonition && currentMember" type="info" class="mb-4">
|
|
||||||
Creating and editing project versions can now be done directly from the
|
|
||||||
<NuxtLink to="settings/versions" class="font-medium text-blue hover:underline"
|
|
||||||
>project settings</NuxtLink
|
|
||||||
>.
|
|
||||||
<template #actions>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<ButtonStyled color="blue">
|
|
||||||
<button
|
|
||||||
aria-label="Project Settings"
|
|
||||||
class="!shadow-none"
|
|
||||||
@click="() => router.push('settings/versions')"
|
|
||||||
>
|
|
||||||
<SettingsIcon />
|
|
||||||
Edit versions
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled type="transparent">
|
|
||||||
<button
|
|
||||||
aria-label="Dismiss"
|
|
||||||
class="!shadow-none"
|
|
||||||
@click="() => (hideVersionsAdmonition = true)"
|
|
||||||
>
|
|
||||||
Dismiss
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Admonition>
|
|
||||||
|
|
||||||
<ProjectPageVersions
|
|
||||||
v-if="versions.length"
|
|
||||||
:project="project"
|
|
||||||
:versions="versions"
|
|
||||||
:show-files="flags.showVersionFilesInTable"
|
|
||||||
:current-member="!!currentMember"
|
|
||||||
:loaders="tags.loaders"
|
|
||||||
:game-versions="tags.gameVersions"
|
|
||||||
:base-id="baseDropdownId"
|
|
||||||
:version-link="
|
|
||||||
(version) =>
|
|
||||||
`/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding ? version.displayUrlEnding : version.id)}`
|
|
||||||
"
|
|
||||||
:open-modal="currentMember ? () => handleOpenCreateVersionModal() : undefined"
|
|
||||||
>
|
>
|
||||||
<template #actions="{ version }">
|
<SpinnerIcon class="animate-spin" />
|
||||||
<ButtonStyled circular type="transparent">
|
<span>Loading versions...</span>
|
||||||
<a
|
</div>
|
||||||
v-tooltip="`Download`"
|
|
||||||
:href="getPrimaryFile(version).url"
|
|
||||||
class="hover:!bg-button-bg [&>svg]:!text-green"
|
|
||||||
aria-label="Download"
|
|
||||||
@click="emit('onDownload')"
|
|
||||||
>
|
|
||||||
<DownloadIcon aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled v-if="currentMember" circular type="transparent">
|
|
||||||
<OverflowMenu
|
|
||||||
v-tooltip="'Edit version'"
|
|
||||||
class="hover:!bg-button-bg"
|
|
||||||
:dropdown-id="`${baseDropdownId}-edit-${version.id}`"
|
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
id: 'edit-metadata',
|
|
||||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit-details',
|
|
||||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit-files',
|
|
||||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
aria-label="Edit version"
|
|
||||||
>
|
|
||||||
<EditIcon aria-hidden="true" />
|
|
||||||
<template #edit-files>
|
|
||||||
<FileIcon aria-hidden="true" />
|
|
||||||
Edit files
|
|
||||||
</template>
|
|
||||||
<template #edit-details>
|
|
||||||
<InfoIcon aria-hidden="true" />
|
|
||||||
Edit details
|
|
||||||
</template>
|
|
||||||
<template #edit-metadata>
|
|
||||||
<BoxIcon aria-hidden="true" />
|
|
||||||
Edit metadata
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled circular type="transparent">
|
|
||||||
<OverflowMenu
|
|
||||||
v-tooltip="'More options'"
|
|
||||||
class="hover:!bg-button-bg"
|
|
||||||
:dropdown-id="`${baseDropdownId}-${version.id}`"
|
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
id: 'download',
|
|
||||||
color: 'primary',
|
|
||||||
hoverFilled: true,
|
|
||||||
link: getPrimaryFile(version).url,
|
|
||||||
action: () => {
|
|
||||||
emit('onDownload')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new-tab',
|
|
||||||
action: () => {},
|
|
||||||
link: `/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}`,
|
|
||||||
external: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'copy-link',
|
|
||||||
action: () =>
|
|
||||||
copyToClipboard(
|
|
||||||
`https://modrinth.com/${project.project_type}/${
|
|
||||||
project.slug ? project.slug : project.id
|
|
||||||
}/version/${encodeURI(version.displayUrlEnding)}`,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'share',
|
|
||||||
action: () => {},
|
|
||||||
shown: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'report',
|
|
||||||
color: 'red',
|
|
||||||
hoverFilled: true,
|
|
||||||
action: () => (auth.user ? reportVersion(version.id) : navigateTo('/auth/sign-in')),
|
|
||||||
shown: !currentMember,
|
|
||||||
},
|
|
||||||
{ divider: true, shown: currentMember || flags.developerMode },
|
|
||||||
{
|
|
||||||
id: 'copy-id',
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(version.id)
|
|
||||||
},
|
|
||||||
shown: currentMember || flags.developerMode,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'copy-maven',
|
|
||||||
action: () => {
|
|
||||||
copyToClipboard(`maven.modrinth:${project.slug}:${version.id}`)
|
|
||||||
},
|
|
||||||
shown: flags.developerMode,
|
|
||||||
},
|
|
||||||
{ divider: true, shown: !!currentMember },
|
|
||||||
{
|
|
||||||
id: 'edit-metadata',
|
|
||||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
|
||||||
shown: !!currentMember,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit-details',
|
|
||||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
|
||||||
shown: !!currentMember,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit-files',
|
|
||||||
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
|
||||||
shown: !!currentMember,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'delete',
|
|
||||||
color: 'red',
|
|
||||||
hoverFilled: true,
|
|
||||||
action: () => {
|
|
||||||
selectedVersion = version.id
|
|
||||||
deleteVersionModal?.show()
|
|
||||||
},
|
|
||||||
shown: !!currentMember,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
aria-label="More options"
|
|
||||||
>
|
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
|
||||||
<template #download>
|
|
||||||
<DownloadIcon aria-hidden="true" />
|
|
||||||
Download
|
|
||||||
</template>
|
|
||||||
<template #new-tab>
|
|
||||||
<ExternalIcon aria-hidden="true" />
|
|
||||||
Open in new tab
|
|
||||||
</template>
|
|
||||||
<template #copy-link>
|
|
||||||
<LinkIcon aria-hidden="true" />
|
|
||||||
Copy link
|
|
||||||
</template>
|
|
||||||
<template #share>
|
|
||||||
<ShareIcon aria-hidden="true" />
|
|
||||||
Share
|
|
||||||
</template>
|
|
||||||
<template #report>
|
|
||||||
<ReportIcon aria-hidden="true" />
|
|
||||||
Report
|
|
||||||
</template>
|
|
||||||
<template #edit-files>
|
|
||||||
<FileIcon aria-hidden="true" />
|
|
||||||
Edit files
|
|
||||||
</template>
|
|
||||||
<template #edit-details>
|
|
||||||
<InfoIcon aria-hidden="true" />
|
|
||||||
Edit details
|
|
||||||
</template>
|
|
||||||
<template #edit-metadata>
|
|
||||||
<BoxIcon aria-hidden="true" />
|
|
||||||
Edit metadata
|
|
||||||
</template>
|
|
||||||
<template #delete>
|
|
||||||
<TrashIcon aria-hidden="true" />
|
|
||||||
Delete
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
|
||||||
Copy ID
|
|
||||||
</template>
|
|
||||||
<template #copy-maven>
|
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
|
||||||
Copy Maven coordinates
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
</template>
|
|
||||||
</ProjectPageVersions>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<p class="ml-2">
|
<CreateProjectVersionModal
|
||||||
No versions in project. Visit
|
v-if="currentMember"
|
||||||
<NuxtLink to="settings/versions">
|
ref="create-project-version-modal"
|
||||||
<span class="font-medium text-green hover:underline">project settings</span> to
|
></CreateProjectVersionModal>
|
||||||
</NuxtLink>
|
|
||||||
upload your first version.
|
<ConfirmModal
|
||||||
</p>
|
v-if="currentMember"
|
||||||
|
ref="deleteVersionModal"
|
||||||
|
title="Are you sure you want to delete this version?"
|
||||||
|
description="This will remove this version forever (like really forever)."
|
||||||
|
:has-to-type="false"
|
||||||
|
proceed-label="Delete"
|
||||||
|
@proceed="deleteVersion()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Admonition v-if="!hideVersionsAdmonition && currentMember" type="info" class="mb-4">
|
||||||
|
Creating and editing project versions can now be done directly from the
|
||||||
|
<NuxtLink to="settings/versions" class="font-medium text-blue hover:underline"
|
||||||
|
>project settings</NuxtLink
|
||||||
|
>.
|
||||||
|
<template #actions>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<ButtonStyled color="blue">
|
||||||
|
<button
|
||||||
|
aria-label="Project Settings"
|
||||||
|
class="!shadow-none"
|
||||||
|
@click="() => router.push('settings/versions')"
|
||||||
|
>
|
||||||
|
<SettingsIcon />
|
||||||
|
Edit versions
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled type="transparent">
|
||||||
|
<button
|
||||||
|
aria-label="Dismiss"
|
||||||
|
class="!shadow-none"
|
||||||
|
@click="() => (hideVersionsAdmonition = true)"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Admonition>
|
||||||
|
|
||||||
|
<ProjectPageVersions
|
||||||
|
v-if="versions?.length"
|
||||||
|
:project="project"
|
||||||
|
:versions="versions"
|
||||||
|
:show-files="flags.showVersionFilesInTable"
|
||||||
|
:current-member="!!currentMember"
|
||||||
|
:loaders="tags.loaders"
|
||||||
|
:game-versions="tags.gameVersions"
|
||||||
|
:base-id="baseDropdownId"
|
||||||
|
:version-link="
|
||||||
|
(version) =>
|
||||||
|
`/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding ? version.displayUrlEnding : version.id)}`
|
||||||
|
"
|
||||||
|
:open-modal="currentMember ? () => handleOpenCreateVersionModal() : undefined"
|
||||||
|
>
|
||||||
|
<template #actions="{ version }">
|
||||||
|
<ButtonStyled circular type="transparent">
|
||||||
|
<a
|
||||||
|
v-tooltip="`Download`"
|
||||||
|
:href="getPrimaryFile(version).url"
|
||||||
|
class="hover:!bg-button-bg [&>svg]:!text-green"
|
||||||
|
aria-label="Download"
|
||||||
|
@click="emit('onDownload')"
|
||||||
|
>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-if="currentMember" circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
v-tooltip="'Edit version'"
|
||||||
|
class="hover:!bg-button-bg"
|
||||||
|
:dropdown-id="`${baseDropdownId}-edit-${version.id}`"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'edit-metadata',
|
||||||
|
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-details',
|
||||||
|
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-files',
|
||||||
|
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
aria-label="Edit version"
|
||||||
|
>
|
||||||
|
<EditIcon aria-hidden="true" />
|
||||||
|
<template #edit-files>
|
||||||
|
<FileIcon aria-hidden="true" />
|
||||||
|
Edit files
|
||||||
|
</template>
|
||||||
|
<template #edit-details>
|
||||||
|
<InfoIcon aria-hidden="true" />
|
||||||
|
Edit details
|
||||||
|
</template>
|
||||||
|
<template #edit-metadata>
|
||||||
|
<BoxIcon aria-hidden="true" />
|
||||||
|
Edit metadata
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
v-tooltip="'More options'"
|
||||||
|
class="hover:!bg-button-bg"
|
||||||
|
:dropdown-id="`${baseDropdownId}-${version.id}`"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'download',
|
||||||
|
color: 'primary',
|
||||||
|
hoverFilled: true,
|
||||||
|
link: getPrimaryFile(version).url,
|
||||||
|
action: () => {
|
||||||
|
emit('onDownload')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new-tab',
|
||||||
|
action: () => {},
|
||||||
|
link: `/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||||
|
external: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-link',
|
||||||
|
action: () =>
|
||||||
|
copyToClipboard(
|
||||||
|
`https://modrinth.com/${project.project_type}/${
|
||||||
|
project.slug ? project.slug : project.id
|
||||||
|
}/version/${encodeURI(version.displayUrlEnding)}`,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'share',
|
||||||
|
action: () => {},
|
||||||
|
shown: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'report',
|
||||||
|
color: 'red',
|
||||||
|
hoverFilled: true,
|
||||||
|
action: () =>
|
||||||
|
auth.user ? reportVersion(version.id) : navigateTo('/auth/sign-in'),
|
||||||
|
shown: !currentMember,
|
||||||
|
},
|
||||||
|
{ divider: true, shown: currentMember || flags.developerMode },
|
||||||
|
{
|
||||||
|
id: 'copy-id',
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(version.id)
|
||||||
|
},
|
||||||
|
shown: currentMember || flags.developerMode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'copy-maven',
|
||||||
|
action: () => {
|
||||||
|
copyToClipboard(`maven.modrinth:${project.slug}:${version.id}`)
|
||||||
|
},
|
||||||
|
shown: flags.developerMode,
|
||||||
|
},
|
||||||
|
{ divider: true, shown: !!currentMember },
|
||||||
|
{
|
||||||
|
id: 'edit-metadata',
|
||||||
|
action: () => handleOpenEditVersionModal(version.id, project.id, 'metadata'),
|
||||||
|
shown: !!currentMember,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-details',
|
||||||
|
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-details'),
|
||||||
|
shown: !!currentMember,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-files',
|
||||||
|
action: () => handleOpenEditVersionModal(version.id, project.id, 'add-files'),
|
||||||
|
shown: !!currentMember,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delete',
|
||||||
|
color: 'red',
|
||||||
|
hoverFilled: true,
|
||||||
|
action: () => {
|
||||||
|
selectedVersion = version.id
|
||||||
|
deleteVersionModal?.show()
|
||||||
|
},
|
||||||
|
shown: !!currentMember,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
aria-label="More options"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #download>
|
||||||
|
<DownloadIcon aria-hidden="true" />
|
||||||
|
Download
|
||||||
|
</template>
|
||||||
|
<template #new-tab>
|
||||||
|
<ExternalIcon aria-hidden="true" />
|
||||||
|
Open in new tab
|
||||||
|
</template>
|
||||||
|
<template #copy-link>
|
||||||
|
<LinkIcon aria-hidden="true" />
|
||||||
|
Copy link
|
||||||
|
</template>
|
||||||
|
<template #share>
|
||||||
|
<ShareIcon aria-hidden="true" />
|
||||||
|
Share
|
||||||
|
</template>
|
||||||
|
<template #report>
|
||||||
|
<ReportIcon aria-hidden="true" />
|
||||||
|
Report
|
||||||
|
</template>
|
||||||
|
<template #edit-files>
|
||||||
|
<FileIcon aria-hidden="true" />
|
||||||
|
Edit files
|
||||||
|
</template>
|
||||||
|
<template #edit-details>
|
||||||
|
<InfoIcon aria-hidden="true" />
|
||||||
|
Edit details
|
||||||
|
</template>
|
||||||
|
<template #edit-metadata>
|
||||||
|
<BoxIcon aria-hidden="true" />
|
||||||
|
Edit metadata
|
||||||
|
</template>
|
||||||
|
<template #delete>
|
||||||
|
<TrashIcon aria-hidden="true" />
|
||||||
|
Delete
|
||||||
|
</template>
|
||||||
|
<template #copy-id>
|
||||||
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
Copy ID
|
||||||
|
</template>
|
||||||
|
<template #copy-maven>
|
||||||
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
Copy Maven coordinates
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</ProjectPageVersions>
|
||||||
|
<template v-else>
|
||||||
|
<p class="ml-2">
|
||||||
|
No versions in project. Visit
|
||||||
|
<NuxtLink to="settings/versions">
|
||||||
|
<span class="font-medium text-green hover:underline">project settings</span> to
|
||||||
|
</NuxtLink>
|
||||||
|
upload your first version.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
@@ -273,6 +285,7 @@ import {
|
|||||||
ReportIcon,
|
ReportIcon,
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
ShareIcon,
|
ShareIcon,
|
||||||
|
SpinnerIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
@@ -286,57 +299,48 @@ import {
|
|||||||
ProjectPageVersions,
|
ProjectPageVersions,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useLocalStorage } from '@vueuse/core'
|
import { useLocalStorage } from '@vueuse/core'
|
||||||
import { useTemplateRef } from 'vue'
|
import { onMounted, useTemplateRef } from 'vue'
|
||||||
|
|
||||||
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
import CreateProjectVersionModal from '~/components/ui/create-project-version/CreateProjectVersionModal.vue'
|
||||||
import { reportVersion } from '~/utils/report-helpers.ts'
|
import { reportVersion } from '~/utils/report-helpers.ts'
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
versions: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const tags = useGeneratedState()
|
const tags = useGeneratedState()
|
||||||
const flags = useFeatureFlags()
|
const flags = useFeatureFlags()
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
|
|
||||||
const client = injectModrinthClient()
|
const client = injectModrinthClient()
|
||||||
const { addNotification } = injectNotificationManager()
|
const { addNotification } = injectNotificationManager()
|
||||||
const { refreshVersions } = injectProjectPageContext()
|
const {
|
||||||
|
projectV2: project,
|
||||||
|
currentMember,
|
||||||
|
refreshVersions,
|
||||||
|
versions,
|
||||||
|
versionsLoading,
|
||||||
|
loadVersions,
|
||||||
|
} = injectProjectPageContext()
|
||||||
|
|
||||||
|
// Load versions on mount (client-side)
|
||||||
|
onMounted(() => {
|
||||||
|
loadVersions()
|
||||||
|
})
|
||||||
|
|
||||||
const deleteVersionModal = ref()
|
const deleteVersionModal = ref()
|
||||||
const selectedVersion = ref(null)
|
const selectedVersion = ref(null)
|
||||||
const createProjectVersionModal = useTemplateRef('create-project-version-modal')
|
const createProjectVersionModal = useTemplateRef('create-project-version-modal')
|
||||||
|
|
||||||
const handleOpenCreateVersionModal = () => {
|
const handleOpenCreateVersionModal = () => {
|
||||||
if (!props.currentMember) return
|
if (!currentMember.value) return
|
||||||
createProjectVersionModal.value?.openCreateVersionModal()
|
createProjectVersionModal.value?.openCreateVersionModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenEditVersionModal = (versionId, projectId, stageId) => {
|
const handleOpenEditVersionModal = (versionId, projectId, stageId) => {
|
||||||
if (!props.currentMember) return
|
if (!currentMember.value) return
|
||||||
createProjectVersionModal.value?.openEditVersionModal(versionId, projectId, stageId)
|
createProjectVersionModal.value?.openEditVersionModal(versionId, projectId, stageId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideVersionsAdmonition = useLocalStorage(
|
const hideVersionsAdmonition = useLocalStorage(
|
||||||
'hideVersionsHasMovedAdmonition',
|
'hideVersionsHasMovedAdmonition',
|
||||||
!props.versions.length,
|
!versions.value?.length,
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits(['onDownload', 'deleteVersion'])
|
const emit = defineEmits(['onDownload', 'deleteVersion'])
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export default defineNuxtPlugin((nuxt) => {
|
|||||||
|
|
||||||
nuxt.vueApp.use(VueQueryPlugin, options)
|
nuxt.vueApp.use(VueQueryPlugin, options)
|
||||||
|
|
||||||
|
// Expose queryClient for middleware and composables
|
||||||
|
nuxt.provide('queryClient', queryClient)
|
||||||
|
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
nuxt.hooks.hook('app:rendered', () => {
|
nuxt.hooks.hook('app:rendered', () => {
|
||||||
vueQueryState.value = dehydrate(queryClient)
|
vueQueryState.value = dehydrate(queryClient)
|
||||||
@@ -21,8 +24,6 @@ export default defineNuxtPlugin((nuxt) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.client) {
|
if (import.meta.client) {
|
||||||
nuxt.hooks.hook('app:created', () => {
|
hydrate(queryClient, vueQueryState.value)
|
||||||
hydrate(queryClient, vueQueryState.value)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { AuthFeature, type NuxtClientConfig, NuxtModrinthClient } from '@modrinth/api-client'
|
import {
|
||||||
|
type AuthConfig,
|
||||||
|
AuthFeature,
|
||||||
|
type FeatureConfig,
|
||||||
|
type NuxtClientConfig,
|
||||||
|
NuxtModrinthClient,
|
||||||
|
} from '@modrinth/api-client'
|
||||||
import type { H3Event } from 'h3'
|
import type { H3Event } from 'h3'
|
||||||
|
|
||||||
async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
|
async function getRateLimitKeyFromSecretsStore(): Promise<string | undefined> {
|
||||||
@@ -27,7 +33,7 @@ export function useServerModrinthClient(options?: ServerModrinthClientOptions):
|
|||||||
new AuthFeature({
|
new AuthFeature({
|
||||||
token: options.authToken,
|
token: options.authToken,
|
||||||
tokenPrefix: '',
|
tokenPrefix: '',
|
||||||
}),
|
} as AuthConfig as FeatureConfig),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const isPermission = (perms?: number, bitflag?: number) => {
|
export const isPermission = (perms?: number | null, bitflag?: number | null) => {
|
||||||
if (!perms || !bitflag) return false
|
if (!perms || !bitflag) return false
|
||||||
return (perms & bitflag) === bitflag
|
return (perms & bitflag) === bitflag
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,4 +114,25 @@ export class LabrinthProjectsV2Module extends AbstractModule {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get dependencies for a project
|
||||||
|
*
|
||||||
|
* @param id - Project ID or slug
|
||||||
|
* @returns Promise resolving to dependency info (projects and versions)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const deps = await client.labrinth.projects_v2.getDependencies('sodium')
|
||||||
|
* console.log(deps.projects) // dependent projects
|
||||||
|
* console.log(deps.versions) // dependent versions
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public async getDependencies(id: string): Promise<Labrinth.Projects.v2.DependencyInfo> {
|
||||||
|
return this.client.request<Labrinth.Projects.v2.DependencyInfo>(`/project/${id}/dependencies`, {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 2,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export namespace Labrinth {
|
|||||||
slug: string
|
slug: string
|
||||||
project_type: ProjectType
|
project_type: ProjectType
|
||||||
team: string
|
team: string
|
||||||
|
organization: string | null
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
body: string
|
body: string
|
||||||
@@ -271,6 +272,11 @@ export namespace Labrinth {
|
|||||||
offset?: number
|
offset?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DependencyInfo {
|
||||||
|
projects: Project[]
|
||||||
|
versions: Labrinth.Versions.v2.Version[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace v3 {
|
export namespace v3 {
|
||||||
@@ -371,8 +377,8 @@ export namespace Labrinth {
|
|||||||
team_id: string
|
team_id: string
|
||||||
description: string
|
description: string
|
||||||
icon_url: string | null
|
icon_url: string | null
|
||||||
color: number
|
color: number | null
|
||||||
members: OrganizationMember[]
|
members: TeamMember[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrganizationMember = {
|
export type OrganizationMember = {
|
||||||
@@ -391,11 +397,12 @@ export namespace Labrinth {
|
|||||||
team_id: string
|
team_id: string
|
||||||
user: Users.v3.User
|
user: Users.v3.User
|
||||||
role: string
|
role: string
|
||||||
permissions: number
|
|
||||||
accepted: boolean
|
|
||||||
payouts_split: number
|
|
||||||
ordering: number
|
|
||||||
is_owner: boolean
|
is_owner: boolean
|
||||||
|
permissions: number | null
|
||||||
|
organization_permissions: number | null
|
||||||
|
accepted: boolean
|
||||||
|
payouts_split: number | null
|
||||||
|
ordering: number
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -416,8 +423,13 @@ export namespace Labrinth {
|
|||||||
|
|
||||||
export type FileType = 'required-resource-pack' | 'optional-resource-pack' | 'unknown'
|
export type FileType = 'required-resource-pack' | 'optional-resource-pack' | 'unknown'
|
||||||
|
|
||||||
|
export type VersionFileHash = {
|
||||||
|
sha512: string
|
||||||
|
sha1: string
|
||||||
|
}
|
||||||
|
|
||||||
export type VersionFile = {
|
export type VersionFile = {
|
||||||
hashes: Record<string, string>
|
hashes: VersionFileHash
|
||||||
url: string
|
url: string
|
||||||
filename: string
|
filename: string
|
||||||
primary: boolean
|
primary: boolean
|
||||||
@@ -471,6 +483,8 @@ export namespace Labrinth {
|
|||||||
export interface GetProjectVersionsParams {
|
export interface GetProjectVersionsParams {
|
||||||
game_versions?: string[]
|
game_versions?: string[]
|
||||||
loaders?: string[]
|
loaders?: string[]
|
||||||
|
include_changelog?: boolean
|
||||||
|
apiVersion?: 2 | 3
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VersionChannel = 'release' | 'beta' | 'alpha'
|
export type VersionChannel = 'release' | 'beta' | 'alpha'
|
||||||
|
|||||||
@@ -28,17 +28,20 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
|||||||
id: string,
|
id: string,
|
||||||
options?: Labrinth.Versions.v3.GetProjectVersionsParams,
|
options?: Labrinth.Versions.v3.GetProjectVersionsParams,
|
||||||
): Promise<Labrinth.Versions.v3.Version[]> {
|
): Promise<Labrinth.Versions.v3.Version[]> {
|
||||||
const params: Record<string, string> = {}
|
const params: Record<string, string | boolean> = {}
|
||||||
if (options?.game_versions?.length) {
|
if (options?.game_versions?.length) {
|
||||||
params.game_versions = JSON.stringify(options.game_versions)
|
params.game_versions = JSON.stringify(options.game_versions)
|
||||||
}
|
}
|
||||||
if (options?.loaders?.length) {
|
if (options?.loaders?.length) {
|
||||||
params.loaders = JSON.stringify(options.loaders)
|
params.loaders = JSON.stringify(options.loaders)
|
||||||
}
|
}
|
||||||
|
if (options?.include_changelog !== undefined) {
|
||||||
|
params.include_changelog = options.include_changelog
|
||||||
|
}
|
||||||
|
|
||||||
return this.client.request<Labrinth.Versions.v3.Version[]>(`/project/${id}/version`, {
|
return this.client.request<Labrinth.Versions.v3.Version[]>(`/project/${id}/version`, {
|
||||||
api: 'labrinth',
|
api: 'labrinth',
|
||||||
version: 2, // TODO: move this to a versions v2 module to keep api-client clean and organized
|
version: options?.apiVersion ?? 2,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
params: Object.keys(params).length > 0 ? params : undefined,
|
params: Object.keys(params).length > 0 ? params : undefined,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ export const coreNags: Nag[] = [
|
|||||||
context.project.source_url ||
|
context.project.source_url ||
|
||||||
context.project.wiki_url ||
|
context.project.wiki_url ||
|
||||||
context.project.discord_url ||
|
context.project.discord_url ||
|
||||||
context.project.donation_urls.length > 0
|
context.project.donation_urls?.length
|
||||||
),
|
),
|
||||||
link: {
|
link: {
|
||||||
path: 'settings/links',
|
path: 'settings/links',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Labrinth } from '@modrinth/api-client'
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
import type { MessageDescriptor } from '@modrinth/ui'
|
import type { MessageDescriptor } from '@modrinth/ui'
|
||||||
import type { User, Version } from '@modrinth/utils'
|
|
||||||
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
import type { FunctionalComponent, SVGAttributes } from 'vue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,11 +24,11 @@ export interface NagContext {
|
|||||||
/**
|
/**
|
||||||
* The versions associated with the project.
|
* The versions associated with the project.
|
||||||
*/
|
*/
|
||||||
versions: Version[]
|
versions: Labrinth.Versions.v2.Version[]
|
||||||
/**
|
/**
|
||||||
* The current project member viewing the nag.
|
* The current project member viewing the nag.
|
||||||
*/
|
*/
|
||||||
currentMember: User
|
currentMember: Labrinth.Users.v2.User
|
||||||
/**
|
/**
|
||||||
* The current route in the application.
|
* The current route in the application.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,14 +17,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
<ButtonStyled circular>
|
<ButtonStyled circular>
|
||||||
<nuxt-link
|
<button
|
||||||
:to="`/project/${props.version.project_id}/version/${props.version.id}`"
|
|
||||||
class="min-w-0"
|
class="min-w-0"
|
||||||
aria-label="Open project page"
|
aria-label="View version"
|
||||||
@click="emit('onNavigate')"
|
@click="
|
||||||
|
emit('onNavigate', `/project/${props.version.project_id}/version/${props.version.id}`)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<ExternalIcon aria-hidden="true" />
|
<ExternalIcon aria-hidden="true" />
|
||||||
</nuxt-link>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -45,5 +46,8 @@ const downloadUrl = computed(() => {
|
|||||||
return primary.url
|
return primary.url
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['onDownload', 'onNavigate'])
|
const emit = defineEmits<{
|
||||||
|
onDownload: []
|
||||||
|
onNavigate: [url: string]
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
import type { Labrinth } from '@modrinth/api-client/src/modules/types'
|
import type { Labrinth } from '@modrinth/api-client/src/modules/types'
|
||||||
// TODO: api client this shit
|
|
||||||
import type { TeamMember } from '@modrinth/utils'
|
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
import { createContext } from '.'
|
import { createContext } from '.'
|
||||||
|
|
||||||
export interface ProjectPageContext {
|
export interface ProjectPageContext {
|
||||||
|
// Data refs
|
||||||
projectV2: Ref<Labrinth.Projects.v2.Project>
|
projectV2: Ref<Labrinth.Projects.v2.Project>
|
||||||
projectV3: Ref<Labrinth.Projects.v3.Project>
|
projectV3: Ref<Labrinth.Projects.v3.Project>
|
||||||
|
currentMember: Ref<Labrinth.Projects.v3.TeamMember | null>
|
||||||
|
allMembers: Ref<Labrinth.Projects.v3.TeamMember[]>
|
||||||
|
organization: Ref<Labrinth.Projects.v3.Organization | null>
|
||||||
|
// Lazy version loading (client-side only)
|
||||||
|
versions: Ref<Labrinth.Versions.v2.Version[] | null>
|
||||||
|
versionsLoading: Ref<boolean>
|
||||||
|
// Lazy dependencies loading (client-side only)
|
||||||
|
dependencies: Ref<Labrinth.Projects.v2.DependencyInfo | null>
|
||||||
|
dependenciesLoading: Ref<boolean>
|
||||||
|
|
||||||
|
// Refresh functions (invalidate + refetch)
|
||||||
refreshProject: () => Promise<void>
|
refreshProject: () => Promise<void>
|
||||||
refreshVersions: () => Promise<void>
|
refreshVersions: () => Promise<void>
|
||||||
currentMember: Ref<TeamMember>
|
refreshMembers: () => Promise<void>
|
||||||
|
refreshOrganization: () => Promise<void>
|
||||||
|
|
||||||
|
// Lazy loading
|
||||||
|
loadVersions: () => Promise<void>
|
||||||
|
loadDependencies: () => Promise<void>
|
||||||
|
|
||||||
|
// Mutation functions
|
||||||
|
patchProject: (data: Record<string, unknown>, quiet?: boolean) => Promise<boolean>
|
||||||
|
patchIcon: (icon: File) => Promise<boolean>
|
||||||
|
setProcessing: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const [injectProjectPageContext, provideProjectPageContext] =
|
export const [injectProjectPageContext, provideProjectPageContext] =
|
||||||
|
|||||||
Reference in New Issue
Block a user