License UI redesign + composition API (#3225)
* refactor(frontend): revamp license page - Add more understandable UI - Field titles - Field description - Use more semantically correct elements - Make paragraph not a label - Rephrase some parts - Fields no longer jump around - Split SPDX-identifier and license name into two seperate fields, for readability - Sort imports - fmt * feat(frontend): encourage license URL on custom license * refactor(frontend): license page to composition + ts - Move to Vue composition API - Move to TypeScript - Move away from vue-multiselect to the dropdown component - Use `formatProjectType` function for typesafety - Remove unused form error highlighting code - Creating typings for built-in licenses - Move standard licenses to licenses.ts util - There are other license-related utils I want to move there eventually - Fix typo in Project license type definition * chore(frontend): fmt * chore(frontend): fmt * feat(frontend): require URL and name for custom license * refactor(frontend): give license or-later checkbox own row * chore(frontend): fmt
This commit is contained in:
parent
a75538c093
commit
affeec82f0
@ -1,61 +1,128 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="universal-card">
|
||||
<div class="adjacent-input">
|
||||
<label for="license-multiselect">
|
||||
<span class="label__title size-card-header">License</span>
|
||||
<span class="label__description">
|
||||
It is very important to choose a proper license for your
|
||||
{{ $formatProjectType(project.project_type).toLowerCase() }}. You may choose one from
|
||||
our list or provide a custom license. You may also provide a custom URL to your chosen
|
||||
license; otherwise, the license text will be displayed.
|
||||
<span v-if="license && license.friendly === 'Custom'" class="label__subdescription">
|
||||
Enter a valid
|
||||
<a href="https://spdx.org/licenses/" target="_blank" rel="noopener" class="text-link">
|
||||
SPDX license identifier</a
|
||||
>
|
||||
in the marked area. If your license does not have a SPDX identifier (for example, if
|
||||
you created the license yourself or if the license is Minecraft-specific), simply
|
||||
check the box and enter the name of the license instead.
|
||||
</span>
|
||||
<span class="label__subdescription">
|
||||
Confused? See our
|
||||
<h2 class="label__title size-card-header">License</h2>
|
||||
<p class="label__description">
|
||||
It is important to choose a proper license for your
|
||||
{{ formatProjectType(project.project_type).toLowerCase() }}. You may choose one from our
|
||||
list or provide a custom license. You may also provide a custom URL to your chosen license;
|
||||
otherwise, the license text will be displayed. See our
|
||||
<a
|
||||
href="https://blog.modrinth.com/licensing-guide/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-link"
|
||||
>
|
||||
licensing guide</a
|
||||
>
|
||||
licensing guide
|
||||
</a>
|
||||
for more information.
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label for="license-multiselect">
|
||||
<span class="label__title">Select a license</span>
|
||||
<span class="label__description">
|
||||
How users are and aren't allowed to use your project.
|
||||
</span>
|
||||
</label>
|
||||
<div class="input-stack">
|
||||
<Multiselect
|
||||
id="license-multiselect"
|
||||
|
||||
<div class="w-1/2">
|
||||
<DropdownSelect
|
||||
v-model="license"
|
||||
name="License selector"
|
||||
:options="builtinLicenses"
|
||||
:display-name="(chosen: BuiltinLicense) => chosen.friendly"
|
||||
placeholder="Select license..."
|
||||
track-by="short"
|
||||
label="friendly"
|
||||
:options="defaultLicenses"
|
||||
:searchable="true"
|
||||
:close-on-select="true"
|
||||
:show-labels="false"
|
||||
:class="{
|
||||
'known-error': license?.short === '' && showKnownErrors,
|
||||
}"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input" v-if="license.requiresOnlyOrLater">
|
||||
<label for="or-later-checkbox">
|
||||
<span class="label__title">Later editions</span>
|
||||
<span class="label__description">
|
||||
The license you selected has an "or later" clause. If you check this box, users may use
|
||||
your project under later editions of the license.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<Checkbox
|
||||
v-if="license?.requiresOnlyOrLater"
|
||||
id="or-later-checkbox"
|
||||
v-model="allowOrLater"
|
||||
:disabled="!hasPermission"
|
||||
description="Allow later editions of this license"
|
||||
description="Allow later editions"
|
||||
class="w-1/2"
|
||||
>
|
||||
Allow later editions of this license
|
||||
Allow later editions
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input">
|
||||
<label for="license-url">
|
||||
<span class="label__title">License URL</span>
|
||||
<span class="label__description" v-if="license?.friendly !== 'Custom'">
|
||||
The web location of the full license text. If you don't provide a link, the license text
|
||||
will be displayed instead.
|
||||
</span>
|
||||
<span class="label__description" v-else>
|
||||
The web location of the full license text. You have to provide a link since this is a
|
||||
custom license.
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div class="w-1/2">
|
||||
<input
|
||||
id="license-url"
|
||||
v-model="licenseUrl"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
:placeholder="license?.friendly !== 'Custom' ? `License URL (optional)` : `License URL`"
|
||||
:disabled="!hasPermission || licenseId === 'LicenseRef-Unknown'"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="adjacent-input" v-if="license?.friendly === 'Custom'">
|
||||
<label for="license-spdx" v-if="!nonSpdxLicense">
|
||||
<span class="label__title">SPDX identifier</span>
|
||||
<span class="label__description">
|
||||
If your license does not have an offical
|
||||
<a href="https://spdx.org/licenses/" target="_blank" rel="noopener" class="text-link">
|
||||
SPDX license identifier</a
|
||||
>, check the box and enter the name of the license instead.
|
||||
</span>
|
||||
</label>
|
||||
<label for="license-name" v-else>
|
||||
<span class="label__title">License name</span>
|
||||
<span class="label__description"
|
||||
>The full name of the license. If the license has a SPDX identifier, please uncheck the
|
||||
checkbox and use the identifier instead.</span
|
||||
>
|
||||
</label>
|
||||
|
||||
<div class="input-stack w-1/2">
|
||||
<input
|
||||
v-if="!nonSpdxLicense"
|
||||
v-model="license.short"
|
||||
id="license-spdx"
|
||||
class="w-full"
|
||||
type="text"
|
||||
maxlength="128"
|
||||
placeholder="SPDX identifier"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
v-model="license.short"
|
||||
id="license-name"
|
||||
class="w-full"
|
||||
type="text"
|
||||
maxlength="128"
|
||||
placeholder="License name"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
v-if="license?.friendly === 'Custom'"
|
||||
v-model="nonSpdxLicense"
|
||||
@ -64,31 +131,18 @@
|
||||
>
|
||||
License does not have a SPDX identifier
|
||||
</Checkbox>
|
||||
<input
|
||||
v-if="license?.friendly === 'Custom'"
|
||||
v-model="license.short"
|
||||
type="text"
|
||||
maxlength="2048"
|
||||
:placeholder="nonSpdxLicense ? 'License name' : 'SPDX identifier'"
|
||||
:class="{
|
||||
'known-error': license.short === '' && showKnownErrors,
|
||||
}"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<input
|
||||
v-model="licenseUrl"
|
||||
type="url"
|
||||
maxlength="2048"
|
||||
placeholder="License URL (optional)"
|
||||
:disabled="!hasPermission || licenseId === 'LicenseRef-Unknown'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-stack">
|
||||
<button
|
||||
type="button"
|
||||
class="iconified-button brand-button"
|
||||
:disabled="!hasChanges || license === null"
|
||||
:disabled="
|
||||
!hasChanges ||
|
||||
!hasPermission ||
|
||||
(license.friendly === 'Custom' && (license.short === '' || licenseUrl === ''))
|
||||
"
|
||||
@click="saveChanges()"
|
||||
>
|
||||
<SaveIcon />
|
||||
@ -99,199 +153,109 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Multiselect from "vue-multiselect";
|
||||
import Checkbox from "~/components/ui/Checkbox";
|
||||
<script setup lang="ts">
|
||||
import { Checkbox, DropdownSelect } from "@modrinth/ui";
|
||||
import {
|
||||
TeamMemberPermission,
|
||||
builtinLicenses,
|
||||
formatProjectType,
|
||||
type BuiltinLicense,
|
||||
type Project,
|
||||
type TeamMember,
|
||||
} from "@modrinth/utils";
|
||||
import { computed, ref, type Ref } from "vue";
|
||||
import SaveIcon from "~/assets/images/utils/save.svg?component";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
Multiselect,
|
||||
Checkbox,
|
||||
SaveIcon,
|
||||
},
|
||||
props: {
|
||||
project: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
currentMember: {
|
||||
type: Object,
|
||||
default() {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
patchProject: {
|
||||
type: Function,
|
||||
default() {
|
||||
return () => {
|
||||
this.$notify({
|
||||
group: "main",
|
||||
title: "An error occurred",
|
||||
text: "Patch project function not found",
|
||||
type: "error",
|
||||
});
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
licenseUrl: "",
|
||||
license: { friendly: "", short: "", requiresOnlyOrLater: false },
|
||||
allowOrLater: this.project.license.id.includes("-or-later"),
|
||||
nonSpdxLicense: this.project.license.id.includes("LicenseRef-"),
|
||||
showKnownErrors: false,
|
||||
};
|
||||
},
|
||||
async setup(props) {
|
||||
const defaultLicenses = shallowRef([
|
||||
{ friendly: "Custom", short: "" },
|
||||
{
|
||||
friendly: "All Rights Reserved/No License",
|
||||
short: "All-Rights-Reserved",
|
||||
},
|
||||
{ friendly: "Apache License 2.0", short: "Apache-2.0" },
|
||||
{
|
||||
friendly: 'BSD 2-Clause "Simplified" License',
|
||||
short: "BSD-2-Clause",
|
||||
},
|
||||
{
|
||||
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
||||
short: "BSD-3-Clause",
|
||||
},
|
||||
{
|
||||
friendly: "CC Zero (Public Domain equivalent)",
|
||||
short: "CC0-1.0",
|
||||
},
|
||||
{ friendly: "CC-BY 4.0", short: "CC-BY-4.0" },
|
||||
{
|
||||
friendly: "CC-BY-SA 4.0",
|
||||
short: "CC-BY-SA-4.0",
|
||||
},
|
||||
{
|
||||
friendly: "CC-BY-NC 4.0",
|
||||
short: "CC-BY-NC-4.0",
|
||||
},
|
||||
{
|
||||
friendly: "CC-BY-NC-SA 4.0",
|
||||
short: "CC-BY-NC-SA-4.0",
|
||||
},
|
||||
{
|
||||
friendly: "CC-BY-ND 4.0",
|
||||
short: "CC-BY-ND-4.0",
|
||||
},
|
||||
{
|
||||
friendly: "CC-BY-NC-ND 4.0",
|
||||
short: "CC-BY-NC-ND-4.0",
|
||||
},
|
||||
{
|
||||
friendly: "GNU Affero General Public License v3",
|
||||
short: "AGPL-3.0",
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: "GNU Lesser General Public License v2.1",
|
||||
short: "LGPL-2.1",
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: "GNU Lesser General Public License v3",
|
||||
short: "LGPL-3.0",
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: "GNU General Public License v2",
|
||||
short: "GPL-2.0",
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: "GNU General Public License v3",
|
||||
short: "GPL-3.0",
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{ friendly: "ISC License", short: "ISC" },
|
||||
{ friendly: "MIT License", short: "MIT" },
|
||||
{ friendly: "Mozilla Public License 2.0", short: "MPL-2.0" },
|
||||
{ friendly: "zlib License", short: "Zlib" },
|
||||
]);
|
||||
const props = defineProps<{
|
||||
project: Project;
|
||||
currentMember: TeamMember | undefined;
|
||||
patchProject: (payload: Object, quiet?: boolean) => Object;
|
||||
}>();
|
||||
|
||||
const licenseUrl = ref(props.project.license.url);
|
||||
const license: Ref<{
|
||||
friendly: string;
|
||||
short: string;
|
||||
requiresOnlyOrLater?: boolean;
|
||||
}> = ref({
|
||||
friendly: "",
|
||||
short: "",
|
||||
requiresOnlyOrLater: false,
|
||||
});
|
||||
|
||||
const licenseId = props.project.license.id;
|
||||
const trimmedLicenseId = licenseId
|
||||
const allowOrLater = ref(props.project.license.id.includes("-or-later"));
|
||||
const nonSpdxLicense = ref(props.project.license.id.includes("LicenseRef-"));
|
||||
|
||||
const oldLicenseId = props.project.license.id;
|
||||
const trimmedLicenseId = oldLicenseId
|
||||
.replaceAll("-only", "")
|
||||
.replaceAll("-or-later", "")
|
||||
.replaceAll("LicenseRef-", "");
|
||||
|
||||
const license = ref(
|
||||
defaultLicenses.value.find((x) => x.short === trimmedLicenseId) ?? {
|
||||
license.value = builtinLicenses.find((x) => x.short === trimmedLicenseId) ?? {
|
||||
friendly: "Custom",
|
||||
short: licenseId.replaceAll("LicenseRef-", ""),
|
||||
},
|
||||
);
|
||||
short: oldLicenseId.replaceAll("LicenseRef-", ""),
|
||||
requiresOnlyOrLater: oldLicenseId.includes("-or-later"),
|
||||
};
|
||||
|
||||
if (licenseId === "LicenseRef-Unknown") {
|
||||
if (oldLicenseId === "LicenseRef-Unknown") {
|
||||
// Mark it as not having a license, forcing the user to select one
|
||||
license.value = {
|
||||
friendly: "Unknown",
|
||||
short: licenseId.replaceAll("LicenseRef-", ""),
|
||||
friendly: "",
|
||||
short: oldLicenseId.replaceAll("LicenseRef-", ""),
|
||||
requiresOnlyOrLater: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
defaultLicenses,
|
||||
licenseUrl,
|
||||
license,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasPermission() {
|
||||
const EDIT_DETAILS = 1 << 2;
|
||||
return (this.currentMember.permissions & EDIT_DETAILS) === EDIT_DETAILS;
|
||||
},
|
||||
licenseId() {
|
||||
const hasPermission = computed(() => {
|
||||
return (props.currentMember?.permissions ?? 0) & TeamMemberPermission.EDIT_DETAILS;
|
||||
});
|
||||
|
||||
const licenseId = computed(() => {
|
||||
let id = "";
|
||||
if (this.license === null) return id;
|
||||
|
||||
if (
|
||||
(this.nonSpdxLicense && this.license.friendly === "Custom") ||
|
||||
this.license.short === "All-Rights-Reserved" ||
|
||||
this.license.short === "Unknown"
|
||||
(nonSpdxLicense && license.value.friendly === "Custom") ||
|
||||
license.value.short === "All-Rights-Reserved" ||
|
||||
license.value.short === "Unknown"
|
||||
) {
|
||||
id += "LicenseRef-";
|
||||
}
|
||||
id += this.license.short;
|
||||
if (this.license.requiresOnlyOrLater) {
|
||||
id += this.allowOrLater ? "-or-later" : "-only";
|
||||
|
||||
id += license.value.short;
|
||||
if (license.value.requiresOnlyOrLater) {
|
||||
id += allowOrLater.value ? "-or-later" : "-only";
|
||||
}
|
||||
if (this.nonSpdxLicense && this.license.friendly === "Custom") {
|
||||
|
||||
if (nonSpdxLicense && license.value.friendly === "Custom") {
|
||||
id = id.replaceAll(" ", "-");
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
patchData() {
|
||||
const data = {};
|
||||
|
||||
if (this.licenseId !== this.project.license.id) {
|
||||
data.license_id = this.licenseId;
|
||||
data.license_url = this.licenseUrl ? this.licenseUrl : null;
|
||||
} else if (this.licenseUrl !== this.project.license.url) {
|
||||
data.license_url = this.licenseUrl ? this.licenseUrl : null;
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
hasChanges() {
|
||||
return Object.keys(this.patchData).length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
saveChanges() {
|
||||
if (this.hasChanges) {
|
||||
this.patchProject(this.patchData);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const patchRequestPayload = computed(() => {
|
||||
const payload: {
|
||||
license_id?: string;
|
||||
license_url?: string | null; // null = remove url
|
||||
} = {};
|
||||
|
||||
if (licenseId.value !== props.project.license.id) {
|
||||
payload.license_id = licenseId.value;
|
||||
}
|
||||
|
||||
if (licenseUrl.value !== props.project.license.url) {
|
||||
payload.license_url = licenseUrl.value ? licenseUrl.value : null;
|
||||
}
|
||||
|
||||
return payload;
|
||||
});
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
return Object.keys(patchRequestPayload.value).length > 0;
|
||||
});
|
||||
|
||||
function saveChanges() {
|
||||
props.patchProject(patchRequestPayload.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
export * from './billing'
|
||||
export * from './highlight'
|
||||
export * from './licenses'
|
||||
export * from './parse'
|
||||
export * from './projects'
|
||||
export * from './types'
|
||||
export * from './users'
|
||||
export * from './utils'
|
||||
export * from './billing'
|
||||
|
||||
export * from './types'
|
||||
|
||||
76
packages/utils/licenses.ts
Normal file
76
packages/utils/licenses.ts
Normal file
@ -0,0 +1,76 @@
|
||||
export interface BuiltinLicense {
|
||||
friendly: string
|
||||
short: string
|
||||
requiresOnlyOrLater?: boolean
|
||||
}
|
||||
|
||||
export const builtinLicenses: BuiltinLicense[] = [
|
||||
{ friendly: 'Custom', short: '' },
|
||||
{
|
||||
friendly: 'All Rights Reserved/No License',
|
||||
short: 'All-Rights-Reserved',
|
||||
},
|
||||
{ friendly: 'Apache License 2.0', short: 'Apache-2.0' },
|
||||
{
|
||||
friendly: 'BSD 2-Clause "Simplified" License',
|
||||
short: 'BSD-2-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'BSD 3-Clause "New" or "Revised" License',
|
||||
short: 'BSD-3-Clause',
|
||||
},
|
||||
{
|
||||
friendly: 'CC Zero (Public Domain equivalent)',
|
||||
short: 'CC0-1.0',
|
||||
},
|
||||
{ friendly: 'CC-BY 4.0', short: 'CC-BY-4.0' },
|
||||
{
|
||||
friendly: 'CC-BY-SA 4.0',
|
||||
short: 'CC-BY-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC 4.0',
|
||||
short: 'CC-BY-NC-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-SA 4.0',
|
||||
short: 'CC-BY-NC-SA-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-ND 4.0',
|
||||
short: 'CC-BY-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'CC-BY-NC-ND 4.0',
|
||||
short: 'CC-BY-NC-ND-4.0',
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Affero General Public License v3',
|
||||
short: 'AGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v2.1',
|
||||
short: 'LGPL-2.1',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU Lesser General Public License v3',
|
||||
short: 'LGPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v2',
|
||||
short: 'GPL-2.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{
|
||||
friendly: 'GNU General Public License v3',
|
||||
short: 'GPL-3.0',
|
||||
requiresOnlyOrLater: true,
|
||||
},
|
||||
{ friendly: 'ISC License', short: 'ISC' },
|
||||
{ friendly: 'MIT License', short: 'MIT' },
|
||||
{ friendly: 'Mozilla Public License 2.0', short: 'MPL-2.0' },
|
||||
{ friendly: 'zlib License', short: 'Zlib' },
|
||||
] as const
|
||||
@ -87,8 +87,7 @@ export interface Project {
|
||||
|
||||
license: {
|
||||
id: string
|
||||
name
|
||||
string
|
||||
name: string
|
||||
url?: string
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user