feat: tags validations
This commit is contained in:
parent
27caf336cc
commit
debcb57f47
@ -6,11 +6,31 @@
|
|||||||
<span class="label__title size-card-header">Tags</span>
|
<span class="label__title size-card-header">Tags</span>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="tooManyTagsWarning && !allTagsSelectedWarning"
|
||||||
|
class="my-2 flex items-center gap-1.5 text-orange"
|
||||||
|
>
|
||||||
|
<TriangleAlertIcon class="my-auto" />
|
||||||
|
{{ tooManyTagsWarning }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="multipleResolutionTagsWarning" class="my-2 flex items-center gap-1.5 text-orange">
|
||||||
|
<TriangleAlertIcon class="my-auto" />
|
||||||
|
{{ multipleResolutionTagsWarning }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="allTagsSelectedWarning" class="my-2 flex items-center gap-1.5 text-red">
|
||||||
|
<TriangleAlertIcon class="my-auto" />
|
||||||
|
<span>{{ allTagsSelectedWarning }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Accurate tagging is important to help people find your
|
Accurate tagging is important to help people find your
|
||||||
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
{{ formatProjectType(project.project_type).toLowerCase() }}. Make sure to select all tags
|
||||||
that apply.
|
that apply.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p v-if="project.versions.length === 0" class="known-errors">
|
<p v-if="project.versions.length === 0" class="known-errors">
|
||||||
Please upload a version first in order to select tags!
|
Please upload a version first in order to select tags!
|
||||||
</p>
|
</p>
|
||||||
@ -112,145 +132,182 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { StarIcon, SaveIcon } from "@modrinth/assets";
|
import { computed, ref } from "vue";
|
||||||
import { formatCategory, formatCategoryHeader, formatProjectType } from "@modrinth/utils";
|
import { StarIcon, SaveIcon, TriangleAlertIcon } from "@modrinth/assets";
|
||||||
|
import {
|
||||||
|
formatCategory,
|
||||||
|
formatCategoryHeader,
|
||||||
|
formatProjectType,
|
||||||
|
sortedCategories,
|
||||||
|
type Project,
|
||||||
|
} from "@modrinth/utils";
|
||||||
import Checkbox from "~/components/ui/Checkbox.vue";
|
import Checkbox from "~/components/ui/Checkbox.vue";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Category {
|
||||||
components: {
|
name: string;
|
||||||
Checkbox,
|
header: string;
|
||||||
SaveIcon,
|
icon?: string;
|
||||||
StarIcon,
|
project_type: string;
|
||||||
},
|
}
|
||||||
props: {
|
|
||||||
project: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
allMembers: {
|
|
||||||
type: Array,
|
|
||||||
default() {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentMember: {
|
|
||||||
type: Object,
|
|
||||||
default() {
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
patchProject: {
|
|
||||||
type: Function,
|
|
||||||
default() {
|
|
||||||
return () => {
|
|
||||||
this.$notify({
|
|
||||||
group: "main",
|
|
||||||
title: "An error occurred",
|
|
||||||
text: "Patch project function not found",
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedTags: this.$sortedCategories().filter(
|
|
||||||
(x) =>
|
|
||||||
x.project_type === this.project.actualProjectType &&
|
|
||||||
(this.project.categories.includes(x.name) ||
|
|
||||||
this.project.additional_categories.includes(x.name)),
|
|
||||||
),
|
|
||||||
featuredTags: this.$sortedCategories().filter(
|
|
||||||
(x) =>
|
|
||||||
x.project_type === this.project.actualProjectType &&
|
|
||||||
this.project.categories.includes(x.name),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
categoryLists() {
|
|
||||||
const lists = {};
|
|
||||||
this.$sortedCategories().forEach((x) => {
|
|
||||||
if (x.project_type === this.project.actualProjectType) {
|
|
||||||
const header = x.header;
|
|
||||||
if (!lists[header]) {
|
|
||||||
lists[header] = [];
|
|
||||||
}
|
|
||||||
lists[header].push(x);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return lists;
|
|
||||||
},
|
|
||||||
patchData() {
|
|
||||||
const data = {};
|
|
||||||
// Promote selected categories to featured if there are less than 3 featured
|
|
||||||
const newFeaturedTags = this.featuredTags.slice();
|
|
||||||
if (newFeaturedTags.length < 1 && this.selectedTags.length > newFeaturedTags.length) {
|
|
||||||
const nonFeaturedCategories = this.selectedTags.filter((x) => !newFeaturedTags.includes(x));
|
|
||||||
|
|
||||||
nonFeaturedCategories
|
interface Props {
|
||||||
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
project: Project & {
|
||||||
.forEach((x) => newFeaturedTags.push(x));
|
actualProjectType: string;
|
||||||
}
|
};
|
||||||
// Convert selected and featured categories to backend-usable arrays
|
allMembers?: any[];
|
||||||
const categories = newFeaturedTags.map((x) => x.name);
|
currentMember?: any;
|
||||||
const additionalCategories = this.selectedTags
|
patchProject?: (data: any) => void;
|
||||||
.filter((x) => !newFeaturedTags.includes(x))
|
}
|
||||||
.map((x) => x.name);
|
|
||||||
|
|
||||||
if (
|
const tags = useTags();
|
||||||
categories.length !== this.project.categories.length ||
|
|
||||||
categories.some((value) => !this.project.categories.includes(value))
|
|
||||||
) {
|
|
||||||
data.categories = categories;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
additionalCategories.length !== this.project.additional_categories.length ||
|
allMembers: () => [],
|
||||||
additionalCategories.some((value) => !this.project.additional_categories.includes(value))
|
currentMember: null,
|
||||||
) {
|
patchProject: () => {
|
||||||
data.additional_categories = additionalCategories;
|
addNotification({
|
||||||
}
|
title: "An error occurred",
|
||||||
|
text: "Patch project function not found",
|
||||||
return data;
|
type: "error",
|
||||||
},
|
});
|
||||||
hasChanges() {
|
|
||||||
return Object.keys(this.patchData).length > 0;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
formatProjectType,
|
|
||||||
formatCategoryHeader,
|
|
||||||
formatCategory,
|
|
||||||
toggleCategory(category) {
|
|
||||||
if (this.selectedTags.includes(category)) {
|
|
||||||
this.selectedTags = this.selectedTags.filter((x) => x !== category);
|
|
||||||
if (this.featuredTags.includes(category)) {
|
|
||||||
this.featuredTags = this.featuredTags.filter((x) => x !== category);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.selectedTags.push(category);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleFeaturedCategory(category) {
|
|
||||||
if (this.featuredTags.includes(category)) {
|
|
||||||
this.featuredTags = this.featuredTags.filter((x) => x !== category);
|
|
||||||
} else {
|
|
||||||
this.featuredTags.push(category);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
saveChanges() {
|
|
||||||
if (this.hasChanges) {
|
|
||||||
this.patchProject(this.patchData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedTags = ref<Category[]>(
|
||||||
|
sortedCategories(tags.value).filter(
|
||||||
|
(x: Category) =>
|
||||||
|
x.project_type === props.project.actualProjectType &&
|
||||||
|
(props.project.categories.includes(x.name) ||
|
||||||
|
props.project.additional_categories.includes(x.name)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const featuredTags = ref<Category[]>(
|
||||||
|
sortedCategories(tags.value).filter(
|
||||||
|
(x: Category) =>
|
||||||
|
x.project_type === props.project.actualProjectType &&
|
||||||
|
props.project.categories.includes(x.name),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const categoryLists = computed(() => {
|
||||||
|
const lists: Record<string, Category[]> = {};
|
||||||
|
sortedCategories(tags.value).forEach((x: Category) => {
|
||||||
|
if (x.project_type === props.project.actualProjectType) {
|
||||||
|
const header = x.header;
|
||||||
|
if (!lists[header]) {
|
||||||
|
lists[header] = [];
|
||||||
|
}
|
||||||
|
lists[header].push(x);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return lists;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Warning computeds based on the nags
|
||||||
|
const tooManyTagsWarning = computed(() => {
|
||||||
|
const tagCount = selectedTags.value.length;
|
||||||
|
if (tagCount > 5) {
|
||||||
|
return `You've selected ${tagCount} tags. Consider reducing to 5 or fewer to keep your project focused and easier to discover.`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const multipleResolutionTagsWarning = computed(() => {
|
||||||
|
if (props.project.project_type !== "resourcepack") return null;
|
||||||
|
|
||||||
|
const resolutionTags = selectedTags.value.filter((tag) =>
|
||||||
|
["16x", "32x", "48x", "64x", "128x", "256x", "512x", "1024x"].includes(tag.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resolutionTags.length > 1) {
|
||||||
|
return `You've selected ${resolutionTags.length} resolution tags (${resolutionTags.map((t) => t.name).join(", ")}). Resource packs should typically only have one resolution tag.`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const allTagsSelectedWarning = computed(() => {
|
||||||
|
const categoriesForProjectType = sortedCategories(tags.value).filter(
|
||||||
|
(x: Category) => x.project_type === props.project.actualProjectType,
|
||||||
|
);
|
||||||
|
const totalSelectedTags = selectedTags.value.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
totalSelectedTags === categoriesForProjectType.length &&
|
||||||
|
categoriesForProjectType.length > 0
|
||||||
|
) {
|
||||||
|
return `You've selected all ${categoriesForProjectType.length} available tags. Please select only the tags that truly apply to your project.`;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const patchData = computed(() => {
|
||||||
|
const data: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
// Promote selected categories to featured if there are less than 3 featured
|
||||||
|
const newFeaturedTags = featuredTags.value.slice();
|
||||||
|
if (newFeaturedTags.length < 1 && selectedTags.value.length > newFeaturedTags.length) {
|
||||||
|
const nonFeaturedCategories = selectedTags.value.filter((x) => !newFeaturedTags.includes(x));
|
||||||
|
|
||||||
|
nonFeaturedCategories
|
||||||
|
.slice(0, Math.min(nonFeaturedCategories.length, 3 - newFeaturedTags.length))
|
||||||
|
.forEach((x) => newFeaturedTags.push(x));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert selected and featured categories to backend-usable arrays
|
||||||
|
const categories = newFeaturedTags.map((x) => x.name);
|
||||||
|
const additionalCategories = selectedTags.value
|
||||||
|
.filter((x) => !newFeaturedTags.includes(x))
|
||||||
|
.map((x) => x.name);
|
||||||
|
|
||||||
|
if (
|
||||||
|
categories.length !== props.project.categories.length ||
|
||||||
|
categories.some((value) => !props.project.categories.includes(value))
|
||||||
|
) {
|
||||||
|
data.categories = categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
additionalCategories.length !== props.project.additional_categories.length ||
|
||||||
|
additionalCategories.some((value) => !props.project.additional_categories.includes(value))
|
||||||
|
) {
|
||||||
|
data.additional_categories = additionalCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasChanges = computed(() => {
|
||||||
|
return Object.keys(patchData.value).length > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleCategory = (category: Category) => {
|
||||||
|
if (selectedTags.value.includes(category)) {
|
||||||
|
selectedTags.value = selectedTags.value.filter((x) => x !== category);
|
||||||
|
if (featuredTags.value.includes(category)) {
|
||||||
|
featuredTags.value = featuredTags.value.filter((x) => x !== category);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedTags.value.push(category);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFeaturedCategory = (category: Category) => {
|
||||||
|
if (featuredTags.value.includes(category)) {
|
||||||
|
featuredTags.value = featuredTags.value.filter((x) => x !== category);
|
||||||
|
} else {
|
||||||
|
featuredTags.value.push(category);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveChanges = () => {
|
||||||
|
if (hasChanges.value) {
|
||||||
|
props.patchProject(patchData.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.label__title {
|
.label__title {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -2,5 +2,6 @@ import type { Nag } from '../types/nags'
|
|||||||
import { coreNags } from './nags/core'
|
import { coreNags } from './nags/core'
|
||||||
import { descriptionNags } from './nags/description'
|
import { descriptionNags } from './nags/description'
|
||||||
import { linksNags } from './nags/links'
|
import { linksNags } from './nags/links'
|
||||||
|
import { tagsNags } from './nags/tags'
|
||||||
|
|
||||||
export default [...coreNags, ...linksNags, ...descriptionNags] as Nag[]
|
export default [...coreNags, ...linksNags, ...descriptionNags, ...tagsNags] as Nag[]
|
||||||
|
|||||||
@ -1,16 +1,27 @@
|
|||||||
import type { Nag, NagContext } from '../../types/nags'
|
import type { Nag, NagContext } from '../../types/nags'
|
||||||
|
|
||||||
|
function getCategories(project: any, tags: any) {
|
||||||
|
return (
|
||||||
|
tags.categories?.filter(
|
||||||
|
(category: any) => category.project_type === project.actualProjectType,
|
||||||
|
) ?? []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const tagsNags: Nag[] = [
|
export const tagsNags: Nag[] = [
|
||||||
{
|
{
|
||||||
id: 'too-many-tags',
|
id: 'too-many-tags',
|
||||||
title: 'Too many tags selected',
|
title: 'Too many tags selected',
|
||||||
description: (context: NagContext) => {
|
description: (context: NagContext) => {
|
||||||
const tagCount = context.project.categories.length
|
const tagCount =
|
||||||
return `You've selected ${tagCount} tags. Consider reducing to 3 or fewer to keep your project focused and easier to discover.`
|
context.project.categories.length + context.project.additional_categories?.length || 0
|
||||||
|
return `You've selected ${tagCount} tags. Consider reducing to 5 or fewer to keep your project focused and easier to discover.`
|
||||||
},
|
},
|
||||||
status: 'warning',
|
status: 'warning',
|
||||||
shouldShow: (context: NagContext) => {
|
shouldShow: (context: NagContext) => {
|
||||||
return context.project.categories.length > 3
|
const tagCount =
|
||||||
|
context.project.categories.length + context.project.additional_categories?.length || 0
|
||||||
|
return tagCount > 5
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
path: 'settings/tags',
|
path: 'settings/tags',
|
||||||
@ -46,14 +57,17 @@ export const tagsNags: Nag[] = [
|
|||||||
id: 'all-tags-selected',
|
id: 'all-tags-selected',
|
||||||
title: 'All tags selected',
|
title: 'All tags selected',
|
||||||
description: (context: NagContext) => {
|
description: (context: NagContext) => {
|
||||||
const totalAvailableTags = context.tags.categories?.length || 0
|
const categoriesForProjectType = getCategories(context.project, context.tags)
|
||||||
|
console.log('categoriesForProjectType', categoriesForProjectType)
|
||||||
|
const totalAvailableTags = categoriesForProjectType.length
|
||||||
return `You've selected all ${totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that truly apply to your project.`
|
return `You've selected all ${totalAvailableTags} available tags. This defeats the purpose of tags, which are meant to help users find relevant projects. Please select only the tags that truly apply to your project.`
|
||||||
},
|
},
|
||||||
status: 'required',
|
status: 'required',
|
||||||
shouldShow: (context: NagContext) => {
|
shouldShow: (context: NagContext) => {
|
||||||
const totalAvailableTags = context.tags.categories?.length || 0
|
const categoriesForProjectType = getCategories(context.project, context.tags)
|
||||||
const selectedTags = context.project.categories.length
|
const totalSelectedTags =
|
||||||
return totalAvailableTags > 0 && selectedTags === totalAvailableTags
|
context.project.categories.length + (context.project.additional_categories?.length || 0)
|
||||||
|
return totalSelectedTags === categoriesForProjectType.length
|
||||||
},
|
},
|
||||||
link: {
|
link: {
|
||||||
path: 'settings/tags',
|
path: 'settings/tags',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user