You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,28 @@
|
||||
<template>
|
||||
<div class="normal-page__content">
|
||||
<div class="universal-card">
|
||||
<h2>Analytics</h2>
|
||||
<div class="normal-page__content">
|
||||
<div class="universal-card">
|
||||
<h2>Analytics</h2>
|
||||
|
||||
<p>
|
||||
This page shows you the analytics for your organization's projects. You can see the number
|
||||
of downloads, page views and revenue earned for all of your projects, as well as the total
|
||||
downloads and page views for each project by country.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
This page shows you the analytics for your organization's projects. You can see the number
|
||||
of downloads, page views and revenue earned for all of your projects, as well as the total
|
||||
downloads and page views for each project by country.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ChartDisplay :projects="projects.map((x) => ({ title: x.name, ...x }))" />
|
||||
</div>
|
||||
<ChartDisplay :projects="projects.map((x) => ({ title: x.name, ...x }))" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ChartDisplay from "~/components/ui/charts/ChartDisplay.vue";
|
||||
import { injectOrganizationContext } from "~/providers/organization-context.ts";
|
||||
import ChartDisplay from '~/components/ui/charts/ChartDisplay.vue'
|
||||
import { injectOrganizationContext } from '~/providers/organization-context.ts'
|
||||
|
||||
const { projects } = injectOrganizationContext();
|
||||
const { projects } = injectOrganizationContext()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.markdown-body {
|
||||
margin-bottom: var(--gap-md);
|
||||
margin-bottom: var(--gap-md);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,219 +1,220 @@
|
||||
<script setup>
|
||||
import { SaveIcon, TrashIcon, UploadIcon } from "@modrinth/assets";
|
||||
import { Avatar, Button, ConfirmModal, FileInput, injectNotificationManager } from "@modrinth/ui";
|
||||
import { injectOrganizationContext } from "~/providers/organization-context.ts";
|
||||
import { SaveIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, ConfirmModal, FileInput, injectNotificationManager } from '@modrinth/ui'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
import { injectOrganizationContext } from '~/providers/organization-context.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const {
|
||||
organization,
|
||||
refresh: refreshOrganization,
|
||||
hasPermission,
|
||||
deleteIcon,
|
||||
patchIcon,
|
||||
patchOrganization,
|
||||
} = injectOrganizationContext();
|
||||
organization,
|
||||
refresh: refreshOrganization,
|
||||
hasPermission,
|
||||
deleteIcon,
|
||||
patchIcon,
|
||||
patchOrganization,
|
||||
} = injectOrganizationContext()
|
||||
|
||||
const icon = ref(null);
|
||||
const deletedIcon = ref(false);
|
||||
const previewImage = ref(null);
|
||||
const icon = ref(null)
|
||||
const deletedIcon = ref(false)
|
||||
const previewImage = ref(null)
|
||||
|
||||
const name = ref(organization.value.name);
|
||||
const slug = ref(organization.value.slug);
|
||||
const name = ref(organization.value.name)
|
||||
const slug = ref(organization.value.slug)
|
||||
|
||||
const summary = ref(organization.value.description);
|
||||
const summary = ref(organization.value.description)
|
||||
|
||||
const patchData = computed(() => {
|
||||
const data = {};
|
||||
if (name.value !== organization.value.name) {
|
||||
data.name = name.value;
|
||||
}
|
||||
if (slug.value !== organization.value.slug) {
|
||||
data.slug = slug.value;
|
||||
}
|
||||
if (summary.value !== organization.value.description) {
|
||||
data.description = summary.value;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
const data = {}
|
||||
if (name.value !== organization.value.name) {
|
||||
data.name = name.value
|
||||
}
|
||||
if (slug.value !== organization.value.slug) {
|
||||
data.slug = slug.value
|
||||
}
|
||||
if (summary.value !== organization.value.description) {
|
||||
data.description = summary.value
|
||||
}
|
||||
return data
|
||||
})
|
||||
|
||||
const hasChanges = computed(() => {
|
||||
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value;
|
||||
});
|
||||
return Object.keys(patchData.value).length > 0 || deletedIcon.value || icon.value
|
||||
})
|
||||
|
||||
const markIconForDeletion = () => {
|
||||
deletedIcon.value = true;
|
||||
icon.value = null;
|
||||
previewImage.value = null;
|
||||
};
|
||||
deletedIcon.value = true
|
||||
icon.value = null
|
||||
previewImage.value = null
|
||||
}
|
||||
|
||||
const showPreviewImage = (files) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
|
||||
icon.value = files[0];
|
||||
deletedIcon.value = false;
|
||||
icon.value = files[0]
|
||||
deletedIcon.value = false
|
||||
|
||||
reader.readAsDataURL(icon.value);
|
||||
reader.onload = (event) => {
|
||||
previewImage.value = event.target.result;
|
||||
};
|
||||
};
|
||||
reader.readAsDataURL(icon.value)
|
||||
reader.onload = (event) => {
|
||||
previewImage.value = event.target.result
|
||||
}
|
||||
}
|
||||
|
||||
const orgId = useRouteId();
|
||||
const orgId = useRouteId()
|
||||
|
||||
const onSaveChanges = useClientTry(async () => {
|
||||
if (hasChanges.value) {
|
||||
await patchOrganization(orgId, patchData.value);
|
||||
}
|
||||
if (hasChanges.value) {
|
||||
await patchOrganization(orgId, patchData.value)
|
||||
}
|
||||
|
||||
if (deletedIcon.value) {
|
||||
await deleteIcon();
|
||||
deletedIcon.value = false;
|
||||
} else if (icon.value) {
|
||||
await patchIcon(icon.value);
|
||||
icon.value = null;
|
||||
}
|
||||
if (deletedIcon.value) {
|
||||
await deleteIcon()
|
||||
deletedIcon.value = false
|
||||
} else if (icon.value) {
|
||||
await patchIcon(icon.value)
|
||||
icon.value = null
|
||||
}
|
||||
|
||||
await refreshOrganization();
|
||||
await refreshOrganization()
|
||||
|
||||
addNotification({
|
||||
title: "Organization updated",
|
||||
text: "Your organization has been updated.",
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
addNotification({
|
||||
title: 'Organization updated',
|
||||
text: 'Your organization has been updated.',
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
const onDeleteOrganization = useClientTry(async () => {
|
||||
await useBaseFetch(`organization/${orgId}`, {
|
||||
method: "DELETE",
|
||||
apiVersion: 3,
|
||||
});
|
||||
await useBaseFetch(`organization/${orgId}`, {
|
||||
method: 'DELETE',
|
||||
apiVersion: 3,
|
||||
})
|
||||
|
||||
addNotification({
|
||||
title: "Organization deleted",
|
||||
text: "Your organization has been deleted.",
|
||||
type: "success",
|
||||
});
|
||||
addNotification({
|
||||
title: 'Organization deleted',
|
||||
text: 'Your organization has been deleted.',
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
await navigateTo("/dashboard/organizations");
|
||||
});
|
||||
await navigateTo('/dashboard/organizations')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="normal-page__content">
|
||||
<ConfirmModal
|
||||
ref="modal_deletion"
|
||||
:title="`Are you sure you want to delete ${organization.name}?`"
|
||||
description="This will delete this organization forever (like *forever* ever)."
|
||||
:has-to-type="true"
|
||||
proceed-label="Delete"
|
||||
:confirmation-text="organization.name"
|
||||
@proceed="onDeleteOrganization"
|
||||
/>
|
||||
<div class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Organization information</span>
|
||||
</h3>
|
||||
</div>
|
||||
<label for="project-icon">
|
||||
<span class="label__title">Icon</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<Avatar
|
||||
:src="deletedIcon ? null : previewImage ? previewImage : organization.icon_url"
|
||||
:alt="organization.name"
|
||||
size="md"
|
||||
class="project__icon"
|
||||
/>
|
||||
<div class="input-stack">
|
||||
<FileInput
|
||||
id="project-icon"
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="btn"
|
||||
prompt="Upload icon"
|
||||
:disabled="!hasPermission"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<Button
|
||||
v-if="!deletedIcon && (previewImage || organization.icon_url)"
|
||||
:disabled="!hasPermission"
|
||||
@click="markIconForDeletion"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove icon
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<ConfirmModal
|
||||
ref="modal_deletion"
|
||||
:title="`Are you sure you want to delete ${organization.name}?`"
|
||||
description="This will delete this organization forever (like *forever* ever)."
|
||||
:has-to-type="true"
|
||||
proceed-label="Delete"
|
||||
:confirmation-text="organization.name"
|
||||
@proceed="onDeleteOrganization"
|
||||
/>
|
||||
<div class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Organization information</span>
|
||||
</h3>
|
||||
</div>
|
||||
<label for="project-icon">
|
||||
<span class="label__title">Icon</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<Avatar
|
||||
:src="deletedIcon ? null : previewImage ? previewImage : organization.icon_url"
|
||||
:alt="organization.name"
|
||||
size="md"
|
||||
class="project__icon"
|
||||
/>
|
||||
<div class="input-stack">
|
||||
<FileInput
|
||||
id="project-icon"
|
||||
:max-size="262144"
|
||||
:show-icon="true"
|
||||
accept="image/png,image/jpeg,image/gif,image/webp"
|
||||
class="btn"
|
||||
prompt="Upload icon"
|
||||
:disabled="!hasPermission"
|
||||
@change="showPreviewImage"
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<Button
|
||||
v-if="!deletedIcon && (previewImage || organization.icon_url)"
|
||||
:disabled="!hasPermission"
|
||||
@click="markIconForDeletion"
|
||||
>
|
||||
<TrashIcon />
|
||||
Remove icon
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="project-name">
|
||||
<span class="label__title">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-name"
|
||||
v-model="name"
|
||||
maxlength="2048"
|
||||
type="text"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
<label for="project-name">
|
||||
<span class="label__title">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="project-name"
|
||||
v-model="name"
|
||||
maxlength="2048"
|
||||
type="text"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
|
||||
<label for="project-slug">
|
||||
<span class="label__title">URL</span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">https://modrinth.com/organization/</div>
|
||||
<input
|
||||
id="project-slug"
|
||||
v-model="slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<label for="project-slug">
|
||||
<span class="label__title">URL</span>
|
||||
</label>
|
||||
<div class="text-input-wrapper">
|
||||
<div class="text-input-wrapper__before">https://modrinth.com/organization/</div>
|
||||
<input
|
||||
id="project-slug"
|
||||
v-model="slug"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label for="project-summary">
|
||||
<span class="label__title">Summary</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper summary-input">
|
||||
<textarea
|
||||
id="project-summary"
|
||||
v-model="summary"
|
||||
maxlength="256"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button color="primary" :disabled="!hasChanges" @click="onSaveChanges">
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Delete organization</span>
|
||||
</h3>
|
||||
</div>
|
||||
<p>
|
||||
Deleting your organization will transfer all of its projects to the organization owner. This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
<Button color="danger" @click="() => $refs.modal_deletion.show()">
|
||||
<TrashIcon />
|
||||
Delete organization
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<label for="project-summary">
|
||||
<span class="label__title">Summary</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper summary-input">
|
||||
<textarea
|
||||
id="project-summary"
|
||||
v-model="summary"
|
||||
maxlength="256"
|
||||
:disabled="!hasPermission"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button color="primary" :disabled="!hasChanges" @click="onSaveChanges">
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Delete organization</span>
|
||||
</h3>
|
||||
</div>
|
||||
<p>
|
||||
Deleting your organization will transfer all of its projects to the organization owner. This
|
||||
action cannot be undone.
|
||||
</p>
|
||||
<Button color="danger" @click="() => $refs.modal_deletion.show()">
|
||||
<TrashIcon />
|
||||
Delete organization
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.summary-input {
|
||||
min-height: 8rem;
|
||||
max-width: 24rem;
|
||||
min-height: 8rem;
|
||||
max-width: 24rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,439 +1,440 @@
|
||||
<template>
|
||||
<div class="normal-page__content">
|
||||
<div class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Manage members</span>
|
||||
</h3>
|
||||
</div>
|
||||
<span class="label">
|
||||
<span class="label__title">Invite a member</span>
|
||||
<span class="label__description">
|
||||
Enter the Modrinth username of the person you'd like to invite to be a member of this
|
||||
organization.
|
||||
</span>
|
||||
</span>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.MANAGE_INVITES,
|
||||
)
|
||||
"
|
||||
@keypress.enter="() => onInviteTeamMember(organization.team, currentUsername)"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<Button
|
||||
color="primary"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.MANAGE_INVITES,
|
||||
)
|
||||
"
|
||||
@click="() => onInviteTeamMember(organization.team_id, currentUsername)"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
Invite
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Leave organization</span>
|
||||
<span class="label__description">
|
||||
Remove yourself as a member of this organization.
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
color="danger"
|
||||
:disabled="currentMember.is_owner"
|
||||
@click="() => onLeaveProject(organization.team_id, auth.user.id)"
|
||||
>
|
||||
<UserRemoveIcon />
|
||||
Leave organization
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allTeamMembers"
|
||||
:key="member.user.id"
|
||||
class="member universal-card"
|
||||
:class="{ open: openTeamMembers.includes(member.user.id) }"
|
||||
>
|
||||
<div class="member-header">
|
||||
<div class="info">
|
||||
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="sm" circle />
|
||||
<div class="text">
|
||||
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
||||
<p>{{ member.user.username }}</p>
|
||||
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
|
||||
</nuxt-link>
|
||||
<p>{{ member.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-buttons">
|
||||
<Badge v-if="member.accepted" type="accepted" />
|
||||
<Badge v-else type="pending" />
|
||||
<Button
|
||||
icon-only
|
||||
transparent
|
||||
class="dropdown-icon"
|
||||
@click="
|
||||
openTeamMembers.indexOf(member.user.id) === -1
|
||||
? openTeamMembers.push(member.user.id)
|
||||
: (openTeamMembers = openTeamMembers.filter((it) => it !== member.user.id))
|
||||
"
|
||||
>
|
||||
<DropdownIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="adjacent-input">
|
||||
<label :for="`member-${member.user.id}-role`">
|
||||
<span class="label__title">Role</span>
|
||||
<span class="label__description">
|
||||
The title of the role that this member plays for this organization.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`member-${member.user.id}-role`"
|
||||
v-model="member.role"
|
||||
type="text"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label :for="`member-${member.user.id}-monetization-weight`">
|
||||
<span class="label__title">Monetization weight</span>
|
||||
<span class="label__description">
|
||||
Relative to all other members' monetization weights, this determines what portion of
|
||||
the organization projects' revenue goes to this member.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`member-${member.user.id}-monetization-weight`"
|
||||
v-model="member.payouts_split"
|
||||
type="number"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!member.is_owner">
|
||||
<span class="label">
|
||||
<span class="label__title">Project permissions</span>
|
||||
</span>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
v-for="[label, permission] in Object.entries(projectPermissions)"
|
||||
:key="permission"
|
||||
:model-value="isPermission(member.permissions, permission)"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER_DEFAULT_PERMISSIONS,
|
||||
) || !isPermission(currentMember.permissions, permission)
|
||||
"
|
||||
:label="permToLabel(label)"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= permission"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!member.is_owner">
|
||||
<span class="label">
|
||||
<span class="label__title">Organization permissions</span>
|
||||
</span>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
v-for="[label, permission] in Object.entries(organizationPermissions)"
|
||||
:key="permission"
|
||||
:model-value="isPermission(member.organization_permissions, permission)"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
) || !isPermission(currentMember.organization_permissions, permission)
|
||||
"
|
||||
:label="permToLabel(label)"
|
||||
@update:model-value="allTeamMembers[index].organization_permissions ^= permission"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="input-group">
|
||||
<Button
|
||||
color="primary"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
)
|
||||
"
|
||||
@click="onUpdateTeamMember(organization.team_id, member)"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!member.is_owner"
|
||||
color="danger"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
) &&
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.REMOVE_MEMBER,
|
||||
)
|
||||
"
|
||||
@click="onRemoveMember(organization.team_id, member)"
|
||||
>
|
||||
<UserRemoveIcon />
|
||||
Remove member
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!member.is_owner && currentMember.is_owner && member.accepted"
|
||||
@click="() => onTransferOwnership(organization.team_id, member.user.id)"
|
||||
>
|
||||
<TransferIcon />
|
||||
Transfer ownership
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="normal-page__content">
|
||||
<div class="universal-card">
|
||||
<div class="label">
|
||||
<h3>
|
||||
<span class="label__title size-card-header">Manage members</span>
|
||||
</h3>
|
||||
</div>
|
||||
<span class="label">
|
||||
<span class="label__title">Invite a member</span>
|
||||
<span class="label__description">
|
||||
Enter the Modrinth username of the person you'd like to invite to be a member of this
|
||||
organization.
|
||||
</span>
|
||||
</span>
|
||||
<div class="input-group">
|
||||
<input
|
||||
id="username"
|
||||
v-model="currentUsername"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.MANAGE_INVITES,
|
||||
)
|
||||
"
|
||||
@keypress.enter="() => onInviteTeamMember(organization.team, currentUsername)"
|
||||
/>
|
||||
<label for="username" class="hidden">Username</label>
|
||||
<Button
|
||||
color="primary"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.MANAGE_INVITES,
|
||||
)
|
||||
"
|
||||
@click="() => onInviteTeamMember(organization.team_id, currentUsername)"
|
||||
>
|
||||
<UserPlusIcon />
|
||||
Invite
|
||||
</Button>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<span class="label">
|
||||
<span class="label__title">Leave organization</span>
|
||||
<span class="label__description">
|
||||
Remove yourself as a member of this organization.
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
color="danger"
|
||||
:disabled="currentMember.is_owner"
|
||||
@click="() => onLeaveProject(organization.team_id, auth.user.id)"
|
||||
>
|
||||
<UserRemoveIcon />
|
||||
Leave organization
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-for="(member, index) in allTeamMembers"
|
||||
:key="member.user.id"
|
||||
class="member universal-card"
|
||||
:class="{ open: openTeamMembers.includes(member.user.id) }"
|
||||
>
|
||||
<div class="member-header">
|
||||
<div class="info">
|
||||
<Avatar :src="member.user.avatar_url" :alt="member.user.username" size="sm" circle />
|
||||
<div class="text">
|
||||
<nuxt-link :to="'/user/' + member.user.username" class="name">
|
||||
<p>{{ member.user.username }}</p>
|
||||
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
|
||||
</nuxt-link>
|
||||
<p>{{ member.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="side-buttons">
|
||||
<Badge v-if="member.accepted" type="accepted" />
|
||||
<Badge v-else type="pending" />
|
||||
<Button
|
||||
icon-only
|
||||
transparent
|
||||
class="dropdown-icon"
|
||||
@click="
|
||||
openTeamMembers.indexOf(member.user.id) === -1
|
||||
? openTeamMembers.push(member.user.id)
|
||||
: (openTeamMembers = openTeamMembers.filter((it) => it !== member.user.id))
|
||||
"
|
||||
>
|
||||
<DropdownIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="adjacent-input">
|
||||
<label :for="`member-${member.user.id}-role`">
|
||||
<span class="label__title">Role</span>
|
||||
<span class="label__description">
|
||||
The title of the role that this member plays for this organization.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`member-${member.user.id}-role`"
|
||||
v-model="member.role"
|
||||
type="text"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="adjacent-input">
|
||||
<label :for="`member-${member.user.id}-monetization-weight`">
|
||||
<span class="label__title">Monetization weight</span>
|
||||
<span class="label__description">
|
||||
Relative to all other members' monetization weights, this determines what portion of
|
||||
the organization projects' revenue goes to this member.
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
:id="`member-${member.user.id}-monetization-weight`"
|
||||
v-model="member.payouts_split"
|
||||
type="number"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!member.is_owner">
|
||||
<span class="label">
|
||||
<span class="label__title">Project permissions</span>
|
||||
</span>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
v-for="[label, permission] in Object.entries(projectPermissions)"
|
||||
:key="permission"
|
||||
:model-value="isPermission(member.permissions, permission)"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER_DEFAULT_PERMISSIONS,
|
||||
) || !isPermission(currentMember.permissions, permission)
|
||||
"
|
||||
:label="permToLabel(label)"
|
||||
@update:model-value="allTeamMembers[index].permissions ^= permission"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="!member.is_owner">
|
||||
<span class="label">
|
||||
<span class="label__title">Organization permissions</span>
|
||||
</span>
|
||||
<div class="permissions">
|
||||
<Checkbox
|
||||
v-for="[label, permission] in Object.entries(organizationPermissions)"
|
||||
:key="permission"
|
||||
:model-value="isPermission(member.organization_permissions, permission)"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
) || !isPermission(currentMember.organization_permissions, permission)
|
||||
"
|
||||
:label="permToLabel(label)"
|
||||
@update:model-value="allTeamMembers[index].organization_permissions ^= permission"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="input-group">
|
||||
<Button
|
||||
color="primary"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
)
|
||||
"
|
||||
@click="onUpdateTeamMember(organization.team_id, member)"
|
||||
>
|
||||
<SaveIcon />
|
||||
Save changes
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!member.is_owner"
|
||||
color="danger"
|
||||
:disabled="
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.EDIT_MEMBER,
|
||||
) &&
|
||||
!isPermission(
|
||||
currentMember.organization_permissions,
|
||||
organizationPermissions.REMOVE_MEMBER,
|
||||
)
|
||||
"
|
||||
@click="onRemoveMember(organization.team_id, member)"
|
||||
>
|
||||
<UserRemoveIcon />
|
||||
Remove member
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!member.is_owner && currentMember.is_owner && member.accepted"
|
||||
@click="() => onTransferOwnership(organization.team_id, member.user.id)"
|
||||
>
|
||||
<TransferIcon />
|
||||
Transfer ownership
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
CrownIcon,
|
||||
DropdownIcon,
|
||||
SaveIcon,
|
||||
TransferIcon,
|
||||
UserPlusIcon,
|
||||
UserXIcon as UserRemoveIcon,
|
||||
} from "@modrinth/assets";
|
||||
import { Avatar, Badge, Button, Checkbox, injectNotificationManager } from "@modrinth/ui";
|
||||
import { ref } from "vue";
|
||||
import { removeTeamMember } from "~/helpers/teams.js";
|
||||
import { injectOrganizationContext } from "~/providers/organization-context.ts";
|
||||
import { isPermission } from "~/utils/permissions.ts";
|
||||
CrownIcon,
|
||||
DropdownIcon,
|
||||
SaveIcon,
|
||||
TransferIcon,
|
||||
UserPlusIcon,
|
||||
UserXIcon as UserRemoveIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Avatar, Badge, Button, Checkbox, injectNotificationManager } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager();
|
||||
const { organization, refresh: refreshOrganization, currentMember } = injectOrganizationContext();
|
||||
import { removeTeamMember } from '~/helpers/teams.js'
|
||||
import { injectOrganizationContext } from '~/providers/organization-context.ts'
|
||||
import { isPermission } from '~/utils/permissions.ts'
|
||||
|
||||
const auth = await useAuth();
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { organization, refresh: refreshOrganization, currentMember } = injectOrganizationContext()
|
||||
|
||||
const currentUsername = ref("");
|
||||
const openTeamMembers = ref([]);
|
||||
const auth = await useAuth()
|
||||
|
||||
const allTeamMembers = ref(organization.value.members);
|
||||
const currentUsername = ref('')
|
||||
const openTeamMembers = ref([])
|
||||
|
||||
const allTeamMembers = ref(organization.value.members)
|
||||
|
||||
watch(
|
||||
() => organization.value,
|
||||
() => {
|
||||
allTeamMembers.value = organization.value.members;
|
||||
},
|
||||
);
|
||||
() => organization.value,
|
||||
() => {
|
||||
allTeamMembers.value = organization.value.members
|
||||
},
|
||||
)
|
||||
|
||||
const projectPermissions = {
|
||||
UPLOAD_VERSION: 1 << 0,
|
||||
DELETE_VERSION: 1 << 1,
|
||||
EDIT_DETAILS: 1 << 2,
|
||||
EDIT_BODY: 1 << 3,
|
||||
MANAGE_INVITES: 1 << 4,
|
||||
REMOVE_MEMBER: 1 << 5,
|
||||
EDIT_MEMBER: 1 << 6,
|
||||
DELETE_PROJECT: 1 << 7,
|
||||
VIEW_ANALYTICS: 1 << 8,
|
||||
VIEW_PAYOUTS: 1 << 9,
|
||||
};
|
||||
UPLOAD_VERSION: 1 << 0,
|
||||
DELETE_VERSION: 1 << 1,
|
||||
EDIT_DETAILS: 1 << 2,
|
||||
EDIT_BODY: 1 << 3,
|
||||
MANAGE_INVITES: 1 << 4,
|
||||
REMOVE_MEMBER: 1 << 5,
|
||||
EDIT_MEMBER: 1 << 6,
|
||||
DELETE_PROJECT: 1 << 7,
|
||||
VIEW_ANALYTICS: 1 << 8,
|
||||
VIEW_PAYOUTS: 1 << 9,
|
||||
}
|
||||
|
||||
const organizationPermissions = {
|
||||
EDIT_DETAILS: 1 << 0,
|
||||
MANAGE_INVITES: 1 << 1,
|
||||
REMOVE_MEMBER: 1 << 2,
|
||||
EDIT_MEMBER: 1 << 3,
|
||||
ADD_PROJECT: 1 << 4,
|
||||
REMOVE_PROJECT: 1 << 5,
|
||||
DELETE_ORGANIZATION: 1 << 6,
|
||||
EDIT_MEMBER_DEFAULT_PERMISSIONS: 1 << 7,
|
||||
};
|
||||
EDIT_DETAILS: 1 << 0,
|
||||
MANAGE_INVITES: 1 << 1,
|
||||
REMOVE_MEMBER: 1 << 2,
|
||||
EDIT_MEMBER: 1 << 3,
|
||||
ADD_PROJECT: 1 << 4,
|
||||
REMOVE_PROJECT: 1 << 5,
|
||||
DELETE_ORGANIZATION: 1 << 6,
|
||||
EDIT_MEMBER_DEFAULT_PERMISSIONS: 1 << 7,
|
||||
}
|
||||
|
||||
const permToLabel = (key) => {
|
||||
const o = key.split("_").join(" ");
|
||||
return o.charAt(0).toUpperCase() + o.slice(1).toLowerCase();
|
||||
};
|
||||
const o = key.split('_').join(' ')
|
||||
return o.charAt(0).toUpperCase() + o.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
const leaveProject = async (teamId, uid) => {
|
||||
await removeTeamMember(teamId, uid);
|
||||
await navigateTo(`/organization/${organization.value.id}`);
|
||||
};
|
||||
await removeTeamMember(teamId, uid)
|
||||
await navigateTo(`/organization/${organization.value.id}`)
|
||||
}
|
||||
|
||||
const onLeaveProject = useClientTry(leaveProject);
|
||||
const onLeaveProject = useClientTry(leaveProject)
|
||||
|
||||
const onInviteTeamMember = useClientTry(async (teamId, username) => {
|
||||
const user = await useBaseFetch(`user/${username}`);
|
||||
const data = {
|
||||
user_id: user.id.trim(),
|
||||
};
|
||||
await useBaseFetch(`team/${teamId}/members`, {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
await refreshOrganization();
|
||||
currentUsername.value = "";
|
||||
addNotification({
|
||||
title: "Member invited",
|
||||
text: `${user.username} has been invited to the organization.`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
const user = await useBaseFetch(`user/${username}`)
|
||||
const data = {
|
||||
user_id: user.id.trim(),
|
||||
}
|
||||
await useBaseFetch(`team/${teamId}/members`, {
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
await refreshOrganization()
|
||||
currentUsername.value = ''
|
||||
addNotification({
|
||||
title: 'Member invited',
|
||||
text: `${user.username} has been invited to the organization.`,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
const onRemoveMember = useClientTry(async (teamId, member) => {
|
||||
await removeTeamMember(teamId, member.user.id);
|
||||
await refreshOrganization();
|
||||
addNotification({
|
||||
title: "Member removed",
|
||||
text: `${member.user.username} has been removed from the organization.`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
await removeTeamMember(teamId, member.user.id)
|
||||
await refreshOrganization()
|
||||
addNotification({
|
||||
title: 'Member removed',
|
||||
text: `${member.user.username} has been removed from the organization.`,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
const onUpdateTeamMember = useClientTry(async (teamId, member) => {
|
||||
const data = !member.is_owner
|
||||
? {
|
||||
permissions: member.permissions,
|
||||
organization_permissions: member.organization_permissions,
|
||||
role: member.role,
|
||||
payouts_split: member.payouts_split,
|
||||
}
|
||||
: {
|
||||
payouts_split: member.payouts_split,
|
||||
role: member.role,
|
||||
};
|
||||
await useBaseFetch(`team/${teamId}/members/${member.user.id}`, {
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
});
|
||||
await refreshOrganization();
|
||||
addNotification({
|
||||
title: "Member updated",
|
||||
text: `${member.user.username} has been updated.`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
const data = !member.is_owner
|
||||
? {
|
||||
permissions: member.permissions,
|
||||
organization_permissions: member.organization_permissions,
|
||||
role: member.role,
|
||||
payouts_split: member.payouts_split,
|
||||
}
|
||||
: {
|
||||
payouts_split: member.payouts_split,
|
||||
role: member.role,
|
||||
}
|
||||
await useBaseFetch(`team/${teamId}/members/${member.user.id}`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
await refreshOrganization()
|
||||
addNotification({
|
||||
title: 'Member updated',
|
||||
text: `${member.user.username} has been updated.`,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
|
||||
const onTransferOwnership = useClientTry(async (teamId, uid) => {
|
||||
const data = {
|
||||
user_id: uid,
|
||||
};
|
||||
await useBaseFetch(`team/${teamId}/owner`, {
|
||||
method: "PATCH",
|
||||
body: data,
|
||||
});
|
||||
await refreshOrganization();
|
||||
addNotification({
|
||||
title: "Ownership transferred",
|
||||
text: `The ownership of ${organization.value.name} has been successfully transferred.`,
|
||||
type: "success",
|
||||
});
|
||||
});
|
||||
const data = {
|
||||
user_id: uid,
|
||||
}
|
||||
await useBaseFetch(`team/${teamId}/owner`, {
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
await refreshOrganization()
|
||||
addNotification({
|
||||
title: 'Ownership transferred',
|
||||
text: `The ownership of ${organization.value.name} has been successfully transferred.`,
|
||||
type: 'success',
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member {
|
||||
.member-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
.member-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
.info {
|
||||
display: flex;
|
||||
|
||||
.text {
|
||||
margin: auto 0 auto 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
.text {
|
||||
margin: auto 0 auto 0.5rem;
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
.name {
|
||||
font-weight: bold;
|
||||
.name {
|
||||
font-weight: bold;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
|
||||
svg {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
}
|
||||
svg {
|
||||
color: var(--color-orange);
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.side-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.dropdown-icon {
|
||||
margin-left: 1rem;
|
||||
.dropdown-icon {
|
||||
margin-left: 1rem;
|
||||
|
||||
svg {
|
||||
transition: 150ms ease transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
svg {
|
||||
transition: 150ms ease transform;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding-top: var(--gap-md);
|
||||
.content {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
padding-top: var(--gap-md);
|
||||
|
||||
.main-info {
|
||||
margin-bottom: var(--gap-lg);
|
||||
}
|
||||
.main-info {
|
||||
margin-bottom: var(--gap-lg);
|
||||
}
|
||||
|
||||
.permissions {
|
||||
margin-bottom: var(--gap-md);
|
||||
max-width: 45rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
.permissions {
|
||||
margin-bottom: var(--gap-md);
|
||||
max-width: 45rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.open {
|
||||
.member-header {
|
||||
.dropdown-icon svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
&.open {
|
||||
.member-header {
|
||||
.dropdown-icon svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.checkbox-outer) {
|
||||
button.checkbox {
|
||||
border: none;
|
||||
}
|
||||
button.checkbox {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user