You've already forked AstralRinth
forked from didirus/AstralRinth
* feat: rough draft of tool * fix: example doc * feat: multiselect chips * feat: conditional actions+messaages + utils for handling conditions * feat: migrate checklist v1 to new format. * fix: lint issues * fix: severity util * feat: README.md * feat: start implementing new moderation checklist * feat: message assembly + fix imports * fix: lint issues * feat: add input suggestions * feat: utility cleanup * fix: icon * chore: remove debug logging * chore: remove debug button * feat: modpack permissions flow into it's own component * feat: icons + use id in stage selection button * Support md/plain text in stages. * fix: checklist not persisting/showing on subpages * feat: message gen + appr/with/deny buttons * feat: better notification placement + queue navigation * fix: default props for futureProjects * fix: modpack perms message * fix: issue with future projects props * fix: tab index + z index fixes * feat: keybinds * fix: file approval types * fix: generate message for non-modpack projects * feat: add generate message to stages dropdown * fix: variables not expanding * feat: requests * fix: empty message approval * fix: issues from sync * chore: add comment for old moderation checklist impl * fix: git artifacts * fix: update visibility logic for stages and actions * fix: cleanup logic for should show * fix: markdown editor accidental edit
425 lines
12 KiB
Vue
425 lines
12 KiB
Vue
<template>
|
|
<div>
|
|
<h2 v-if="modPackData" class="m-0 mb-2 text-lg font-extrabold">
|
|
Modpack permissions ({{ Math.min(modPackData.length, currentIndex + 1) }} /
|
|
{{ modPackData.length }})
|
|
</h2>
|
|
|
|
<div v-if="!modPackData">Loading data...</div>
|
|
|
|
<div v-else-if="modPackData.length === 0">
|
|
<p>All permissions obtained. You may skip this step!</p>
|
|
</div>
|
|
|
|
<div v-else-if="!modPackData[currentIndex]">
|
|
<p>All permission checks complete!</p>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<div v-if="modPackData[currentIndex].type === 'unknown'">
|
|
<p>What is the approval type of {{ modPackData[currentIndex].file_name }}?</p>
|
|
<div class="input-group">
|
|
<ButtonStyled
|
|
v-for="(option, index) in fileApprovalTypes"
|
|
:key="index"
|
|
:color="modPackData[currentIndex].status === option.id ? 'brand' : 'standard'"
|
|
@click="setStatus(currentIndex, option.id)"
|
|
>
|
|
<button>
|
|
{{ option.name }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
<div v-if="modPackData[currentIndex].status !== 'unidentified'" class="flex flex-col gap-1">
|
|
<label for="proof">
|
|
<span class="label__title">Proof</span>
|
|
</label>
|
|
<input
|
|
id="proof"
|
|
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).proof"
|
|
type="text"
|
|
autocomplete="off"
|
|
placeholder="Enter proof of status..."
|
|
@input="persistAll()"
|
|
/>
|
|
<label for="link">
|
|
<span class="label__title">Link</span>
|
|
</label>
|
|
<input
|
|
id="link"
|
|
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).url"
|
|
type="text"
|
|
autocomplete="off"
|
|
placeholder="Enter link of project..."
|
|
@input="persistAll()"
|
|
/>
|
|
<label for="title">
|
|
<span class="label__title">Title</span>
|
|
</label>
|
|
<input
|
|
id="title"
|
|
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).title"
|
|
type="text"
|
|
autocomplete="off"
|
|
placeholder="Enter title of project..."
|
|
@input="persistAll()"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else-if="modPackData[currentIndex].type === 'flame'">
|
|
<p>
|
|
What is the approval type of {{ modPackData[currentIndex].title }} (<a
|
|
:href="modPackData[currentIndex].url"
|
|
target="_blank"
|
|
class="text-link"
|
|
>{{ modPackData[currentIndex].url }}</a
|
|
>)?
|
|
</p>
|
|
<div class="input-group">
|
|
<ButtonStyled
|
|
v-for="(option, index) in fileApprovalTypes"
|
|
:key="index"
|
|
:color="modPackData[currentIndex].status === option.id ? 'brand' : 'standard'"
|
|
@click="setStatus(currentIndex, option.id)"
|
|
>
|
|
<button>
|
|
{{ option.name }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="
|
|
['unidentified', 'no', 'with-attribution'].includes(
|
|
modPackData[currentIndex].status || '',
|
|
)
|
|
"
|
|
>
|
|
<p v-if="modPackData[currentIndex].status === 'unidentified'">
|
|
Does this project provide identification and permission for
|
|
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
|
>?
|
|
</p>
|
|
<p v-else-if="modPackData[currentIndex].status === 'with-attribution'">
|
|
Does this project provide attribution for
|
|
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
|
>?
|
|
</p>
|
|
<p v-else>
|
|
Does this project provide proof of permission for
|
|
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
|
>?
|
|
</p>
|
|
<div class="input-group">
|
|
<ButtonStyled
|
|
v-for="(option, index) in filePermissionTypes"
|
|
:key="index"
|
|
:color="modPackData[currentIndex].approved === option.id ? 'brand' : 'standard'"
|
|
@click="setApproval(currentIndex, option.id)"
|
|
>
|
|
<button>
|
|
{{ option.name }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 flex gap-2">
|
|
<ButtonStyled>
|
|
<button :disabled="currentIndex <= 0" @click="goToPrevious">
|
|
<LeftArrowIcon aria-hidden="true" />
|
|
Previous
|
|
</button>
|
|
</ButtonStyled>
|
|
<ButtonStyled v-if="modPackData && currentIndex < modPackData.length" color="blue">
|
|
<button :disabled="!canGoNext" @click="goToNext">
|
|
<RightArrowIcon aria-hidden="true" />
|
|
{{ currentIndex + 1 >= modPackData.length ? "Complete" : "Next" }}
|
|
</button>
|
|
</ButtonStyled>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
|
|
import type {
|
|
ModerationJudgements,
|
|
ModerationModpackItem,
|
|
ModerationModpackResponse,
|
|
ModerationUnknownModpackItem,
|
|
ModerationFlameModpackItem,
|
|
ModerationModpackPermissionApprovalType,
|
|
ModerationPermissionType,
|
|
} from "@modrinth/utils";
|
|
import { ButtonStyled } from "@modrinth/ui";
|
|
import { ref, computed, watch, onMounted } from "vue";
|
|
import { useLocalStorage } from "@vueuse/core";
|
|
|
|
const props = defineProps<{
|
|
projectId: string;
|
|
modelValue?: ModerationJudgements;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
complete: [];
|
|
"update:modelValue": [judgements: ModerationJudgements];
|
|
}>();
|
|
|
|
const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
|
`modpack-permissions-${props.projectId}`,
|
|
null,
|
|
{
|
|
serializer: {
|
|
read: (v: any) => (v ? JSON.parse(v) : null),
|
|
write: (v: any) => JSON.stringify(v),
|
|
},
|
|
},
|
|
);
|
|
|
|
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
|
|
|
const modPackData = ref<ModerationModpackItem[] | null>(null);
|
|
const currentIndex = ref(0);
|
|
|
|
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
|
{
|
|
id: "yes",
|
|
name: "Yes",
|
|
},
|
|
{
|
|
id: "with-attribution-and-source",
|
|
name: "With attribution and source",
|
|
},
|
|
{
|
|
id: "with-attribution",
|
|
name: "With attribution",
|
|
},
|
|
{
|
|
id: "no",
|
|
name: "No",
|
|
},
|
|
{
|
|
id: "permanent-no",
|
|
name: "Permanent no",
|
|
},
|
|
{
|
|
id: "unidentified",
|
|
name: "Unidentified",
|
|
},
|
|
];
|
|
|
|
const filePermissionTypes: ModerationPermissionType[] = [
|
|
{ id: "yes", name: "Yes" },
|
|
{ id: "no", name: "No" },
|
|
];
|
|
|
|
function persistAll() {
|
|
persistedModPackData.value = modPackData.value;
|
|
persistedIndex.value = currentIndex.value;
|
|
}
|
|
|
|
watch(
|
|
modPackData,
|
|
(newValue) => {
|
|
persistedModPackData.value = newValue;
|
|
},
|
|
{ deep: true },
|
|
);
|
|
|
|
watch(currentIndex, (newValue) => {
|
|
persistedIndex.value = newValue;
|
|
});
|
|
|
|
function loadPersistedData(): void {
|
|
if (persistedModPackData.value) {
|
|
modPackData.value = persistedModPackData.value;
|
|
}
|
|
currentIndex.value = persistedIndex.value;
|
|
}
|
|
|
|
function clearPersistedData(): void {
|
|
persistedModPackData.value = null;
|
|
persistedIndex.value = 0;
|
|
}
|
|
|
|
async function fetchModPackData(): Promise<void> {
|
|
try {
|
|
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
|
internal: true,
|
|
})) as ModerationModpackResponse;
|
|
const sortedData: ModerationModpackItem[] = [
|
|
...Object.entries(data.unknown_files || {})
|
|
.map(
|
|
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
|
sha1,
|
|
file_name: fileName,
|
|
type: "unknown",
|
|
status: null,
|
|
approved: null,
|
|
proof: "",
|
|
url: "",
|
|
title: "",
|
|
}),
|
|
)
|
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
|
...Object.entries(data.flame_files || {})
|
|
.map(
|
|
([sha1, info]): ModerationFlameModpackItem => ({
|
|
sha1,
|
|
file_name: info.file_name,
|
|
type: "flame",
|
|
status: null,
|
|
approved: null,
|
|
id: info.id,
|
|
title: info.title || info.file_name,
|
|
url: info.url || `https://www.curseforge.com/minecraft/mc-mods/${info.id}`,
|
|
}),
|
|
)
|
|
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
|
];
|
|
|
|
if (modPackData.value) {
|
|
const existingMap = new Map(modPackData.value.map((item) => [item.sha1, item]));
|
|
|
|
sortedData.forEach((item) => {
|
|
const existing = existingMap.get(item.sha1);
|
|
if (existing) {
|
|
Object.assign(item, {
|
|
status: existing.status,
|
|
approved: existing.approved,
|
|
...(item.type === "unknown" && {
|
|
proof: (existing as ModerationUnknownModpackItem).proof || "",
|
|
url: (existing as ModerationUnknownModpackItem).url || "",
|
|
title: (existing as ModerationUnknownModpackItem).title || "",
|
|
}),
|
|
...(item.type === "flame" && {
|
|
url: (existing as ModerationFlameModpackItem).url || item.url,
|
|
title: (existing as ModerationFlameModpackItem).title || item.title,
|
|
}),
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
modPackData.value = sortedData;
|
|
persistAll();
|
|
} catch (error) {
|
|
console.error("Failed to fetch modpack data:", error);
|
|
modPackData.value = [];
|
|
persistAll();
|
|
}
|
|
}
|
|
|
|
function goToPrevious(): void {
|
|
if (currentIndex.value > 0) {
|
|
currentIndex.value--;
|
|
persistAll();
|
|
}
|
|
}
|
|
|
|
function goToNext(): void {
|
|
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
|
currentIndex.value++;
|
|
|
|
if (currentIndex.value >= modPackData.value.length) {
|
|
const judgements = getJudgements();
|
|
emit("update:modelValue", judgements);
|
|
emit("complete");
|
|
clearPersistedData();
|
|
} else {
|
|
persistAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
function setStatus(index: number, status: ModerationModpackPermissionApprovalType["id"]): void {
|
|
if (modPackData.value && modPackData.value[index]) {
|
|
modPackData.value[index].status = status;
|
|
modPackData.value[index].approved = null;
|
|
persistAll();
|
|
emit("update:modelValue", getJudgements());
|
|
}
|
|
}
|
|
|
|
function setApproval(index: number, approved: ModerationPermissionType["id"]): void {
|
|
if (modPackData.value && modPackData.value[index]) {
|
|
modPackData.value[index].approved = approved;
|
|
persistAll();
|
|
emit("update:modelValue", getJudgements());
|
|
}
|
|
}
|
|
|
|
const canGoNext = computed(() => {
|
|
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false;
|
|
const current = modPackData.value[currentIndex.value];
|
|
return current.status !== null;
|
|
});
|
|
|
|
function getJudgements(): ModerationJudgements {
|
|
if (!modPackData.value) return {};
|
|
|
|
const judgements: ModerationJudgements = {};
|
|
|
|
modPackData.value.forEach((item) => {
|
|
if (item.status && item.status !== "unidentified") {
|
|
if (item.type === "flame") {
|
|
judgements[item.sha1] = {
|
|
type: "flame",
|
|
id: item.id,
|
|
status: item.status,
|
|
link: item.url,
|
|
title: item.title,
|
|
file_name: item.file_name,
|
|
};
|
|
} else if (item.type === "unknown") {
|
|
judgements[item.sha1] = {
|
|
type: "unknown",
|
|
status: item.status,
|
|
proof: item.proof,
|
|
link: item.url,
|
|
title: item.title,
|
|
file_name: item.file_name,
|
|
};
|
|
}
|
|
}
|
|
});
|
|
|
|
return judgements;
|
|
}
|
|
|
|
onMounted(() => {
|
|
loadPersistedData();
|
|
if (!modPackData.value) {
|
|
fetchModPackData();
|
|
}
|
|
});
|
|
|
|
watch(
|
|
() => props.projectId,
|
|
() => {
|
|
clearPersistedData();
|
|
loadPersistedData();
|
|
if (!modPackData.value) {
|
|
fetchModPackData();
|
|
}
|
|
},
|
|
);
|
|
</script>
|
|
|
|
<style scoped>
|
|
.input-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
margin-top: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.modpack-buttons {
|
|
margin-top: 1rem;
|
|
}
|
|
</style>
|