Application & pat ui improvements (#5271)

* Add categories, make localizable

* Run fix

* Run prepr

* Improve pat modal ui

* Fix pat token actions

* Make scope category localization shared

* Fix casing

* Fix casing

---------

Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
Creeperkatze
2026-02-02 19:20:28 +01:00
committed by GitHub
parent 56c8bb1950
commit 3f6c79b00d
4 changed files with 529 additions and 79 deletions

View File

@@ -323,6 +323,57 @@ export const scopeMessages = defineMessages({
}, },
}) })
export const scopeCategoryMessages = defineMessages({
categoryUserAccount: {
id: 'scopes.category.user-account',
defaultMessage: 'User account',
},
categoryProjects: {
id: 'scopes.category.projects',
defaultMessage: 'Projects',
},
categoryVersions: {
id: 'scopes.category.versions',
defaultMessage: 'Versions',
},
categoryCollections: {
id: 'scopes.category.collections',
defaultMessage: 'Collections',
},
categoryOrganizations: {
id: 'scopes.category.organizations',
defaultMessage: 'Organizations',
},
categoryReports: {
id: 'scopes.category.reports',
defaultMessage: 'Reports',
},
categoryThreads: {
id: 'scopes.category.threads',
defaultMessage: 'Threads',
},
categoryPats: {
id: 'scopes.category.pats',
defaultMessage: 'PATs',
},
categorySessions: {
id: 'scopes.category.sessions',
defaultMessage: 'Sessions',
},
categoryNotifications: {
id: 'scopes.category.notifications',
defaultMessage: 'Notifications',
},
categoryPayouts: {
id: 'scopes.category.payouts',
defaultMessage: 'Payouts',
},
categoryAnalytics: {
id: 'scopes.category.analytics',
defaultMessage: 'Analytics',
},
})
const scopeDefinitions = [ const scopeDefinitions = [
{ {
id: 'USER_READ_EMAIL', id: 'USER_READ_EMAIL',

View File

@@ -2444,6 +2444,42 @@
"scopes.analytics.label": { "scopes.analytics.label": {
"message": "Read analytics" "message": "Read analytics"
}, },
"scopes.category.analytics": {
"message": "Analytics"
},
"scopes.category.collections": {
"message": "Collections"
},
"scopes.category.notifications": {
"message": "Notifications"
},
"scopes.category.organizations": {
"message": "Organizations"
},
"scopes.category.pats": {
"message": "PATs"
},
"scopes.category.payouts": {
"message": "Payouts"
},
"scopes.category.projects": {
"message": "Projects"
},
"scopes.category.reports": {
"message": "Reports"
},
"scopes.category.sessions": {
"message": "Sessions"
},
"scopes.category.threads": {
"message": "Threads"
},
"scopes.category.user-account": {
"message": "User account"
},
"scopes.category.versions": {
"message": "Versions"
},
"scopes.collectionCreate.description": { "scopes.collectionCreate.description": {
"message": "Create collections" "message": "Create collections"
}, },
@@ -2741,6 +2777,102 @@
"servers.plan.small.name": { "servers.plan.small.name": {
"message": "Small" "message": "Small"
}, },
"settings.applications.about": {
"message": "About"
},
"settings.applications.button.add-more": {
"message": "Add more"
},
"settings.applications.button.add-redirect-uri": {
"message": "Add a redirect uri"
},
"settings.applications.button.cancel": {
"message": "Cancel"
},
"settings.applications.button.create": {
"message": "Create app"
},
"settings.applications.button.delete": {
"message": "Delete"
},
"settings.applications.button.edit": {
"message": "Edit"
},
"settings.applications.button.new": {
"message": "New application"
},
"settings.applications.button.save-changes": {
"message": "Save changes"
},
"settings.applications.button.upload-icon": {
"message": "Upload icon"
},
"settings.applications.client-id": {
"message": "Client ID"
},
"settings.applications.client-secret": {
"message": "Client secret"
},
"settings.applications.created-on": {
"message": "Created on {date}"
},
"settings.applications.delete.confirm.button": {
"message": "Delete this application"
},
"settings.applications.delete.confirm.description": {
"message": "This will permanently delete this application and revoke all access tokens. (forever!)"
},
"settings.applications.delete.confirm.title": {
"message": "Are you sure you want to delete this application?"
},
"settings.applications.description.intro": {
"message": "Applications can be used to authenticate Modrinth's users with your products. For more information, see <docs-link>Modrinth's API documentation</docs-link>."
},
"settings.applications.field.description": {
"message": "Description"
},
"settings.applications.field.description.placeholder": {
"message": "Enter the application's description..."
},
"settings.applications.field.icon": {
"message": "Icon"
},
"settings.applications.field.name": {
"message": "Name"
},
"settings.applications.field.name.placeholder": {
"message": "Enter the application's name..."
},
"settings.applications.field.redirect-uri.placeholder": {
"message": "https://example.com/auth/callback"
},
"settings.applications.field.redirect-uris": {
"message": "Redirect uris"
},
"settings.applications.field.scopes": {
"message": "Scopes"
},
"settings.applications.field.url": {
"message": "URL"
},
"settings.applications.field.url.placeholder": {
"message": "https://example.com"
},
"settings.applications.modal.header": {
"message": "Application information"
},
"settings.applications.notification.error.title": {
"message": "An error occurred"
},
"settings.applications.notification.icon-updated.description": {
"message": "Your application icon has been updated."
},
"settings.applications.notification.icon-updated.title": {
"message": "Icon updated"
},
"settings.applications.secret.disclaimer": {
"message": "Save your secret now, it will be hidden after you leave this page!"
},
"settings.billing.modal.cancel.action": { "settings.billing.modal.cancel.action": {
"message": "Cancel subscription" "message": "Cancel subscription"
}, },

View File

@@ -2,29 +2,33 @@
<div class="universal-card"> <div class="universal-card">
<ConfirmModal <ConfirmModal
ref="modal_confirm" ref="modal_confirm"
title="Are you sure you want to delete this application?" :title="formatMessage(messages.deleteConfirmTitle)"
description="This will permanently delete this application and revoke all access tokens. (forever!)" :description="formatMessage(messages.deleteConfirmDescription)"
proceed-label="Delete this application" :proceed-label="formatMessage(messages.deleteConfirmButton)"
@proceed="removeApp(editingId)" @proceed="removeApp(editingId)"
/> />
<Modal ref="appModal" header="Application information"> <Modal ref="appModal" :header="formatMessage(messages.modalHeader)">
<div class="universal-modal"> <div class="universal-modal">
<label for="app-name"><span class="label__title">Name</span> </label> <label for="app-name"
><span class="label__title">{{ formatMessage(messages.nameLabel) }}</span>
</label>
<input <input
id="app-name" id="app-name"
v-model="name" v-model="name"
maxlength="2048" maxlength="2048"
type="text" type="text"
autocomplete="off" autocomplete="off"
placeholder="Enter the application's name..." :placeholder="formatMessage(messages.namePlaceholder)"
/> />
<label v-if="editingId" for="app-icon"><span class="label__title">Icon</span> </label> <label v-if="editingId" for="app-icon"
><span class="label__title">{{ formatMessage(messages.iconLabel) }}</span>
</label>
<div v-if="editingId" class="icon-submission"> <div v-if="editingId" class="icon-submission">
<Avatar size="md" :src="icon" /> <Avatar size="md" :src="icon" />
<FileInput <FileInput
:max-size="262144" :max-size="262144"
class="btn" class="btn"
prompt="Upload icon" :prompt="formatMessage(messages.uploadIcon)"
accept="image/png,image/jpeg,image/gif,image/webp" accept="image/png,image/jpeg,image/gif,image/webp"
@change="onImageSelection" @change="onImageSelection"
> >
@@ -32,7 +36,7 @@
</FileInput> </FileInput>
</div> </div>
<label v-if="editingId" for="app-url"> <label v-if="editingId" for="app-url">
<span class="label__title">URL</span> <span class="label__title">{{ formatMessage(messages.urlLabel) }}</span>
</label> </label>
<input <input
v-if="editingId" v-if="editingId"
@@ -41,10 +45,10 @@
maxlength="255" maxlength="255"
type="url" type="url"
autocomplete="off" autocomplete="off"
placeholder="https://example.com" :placeholder="formatMessage(messages.urlPlaceholder)"
/> />
<label v-if="editingId" for="app-description"> <label v-if="editingId" for="app-description">
<span class="label__title">Description</span> <span class="label__title">{{ formatMessage(messages.descriptionLabel) }}</span>
</label> </label>
<textarea <textarea
v-if="editingId" v-if="editingId"
@@ -54,19 +58,33 @@
maxlength="255" maxlength="255"
type="text" type="text"
autocomplete="off" autocomplete="off"
placeholder="Enter the application's description..." :placeholder="formatMessage(messages.descriptionPlaceholder)"
/> />
<label for="app-scopes"><span class="label__title">Scopes</span> </label> <label for="app-scopes"
<div id="app-scopes" class="checkboxes"> ><span class="label__title">{{ formatMessage(messages.scopesLabel) }}</span>
<Checkbox </label>
v-for="scope in scopeList" <div
:key="scope" id="app-scopes"
:label="scopesToLabels(getScopeValue(scope)).join(', ')" class="scope-items mt-2 grid grid-cols-1 gap-x-6 gap-y-4 min-[600px]:grid-cols-2"
:model-value="hasScope(scopesVal, scope)" >
@update:model-value="() => (scopesVal = toggleScope(scopesVal, scope))" <div v-for="category in scopeCategories" :key="category.name" class="flex flex-col gap-2">
/> <h4 class="m-0 border-b border-divider pb-1 text-base font-bold text-contrast">
{{ category.name }}
</h4>
<div class="flex flex-col gap-2">
<Checkbox
v-for="scope in category.scopes"
:key="scope"
:label="scopesToLabels(getScopeValue(scope)).join(', ')"
:model-value="hasScope(scopesVal, scope)"
@update:model-value="() => (scopesVal = toggleScope(scopesVal, scope))"
/>
</div>
</div>
</div> </div>
<label for="app-redirect-uris"><span class="label__title">Redirect uris</span> </label> <label for="app-redirect-uris" class="mt-4"
><span class="label__title">{{ formatMessage(messages.redirectUrisLabel) }}</span>
</label>
<div class="uri-input-list"> <div class="uri-input-list">
<div v-for="(_, index) in redirectUris" :key="index"> <div v-for="(_, index) in redirectUris" :key="index">
<div class="input-group url-input-group-fixes"> <div class="input-group url-input-group-fixes">
@@ -75,7 +93,7 @@
maxlength="2048" maxlength="2048"
type="url" type="url"
autocomplete="off" autocomplete="off"
placeholder="https://example.com/auth/callback" :placeholder="formatMessage(messages.redirectUriPlaceholder)"
/> />
<Button v-if="index !== 0" icon-only @click="() => redirectUris.splice(index, 1)"> <Button v-if="index !== 0" icon-only @click="() => redirectUris.splice(index, 1)">
<TrashIcon /> <TrashIcon />
@@ -86,13 +104,13 @@
icon-only icon-only
@click="() => redirectUris.push('')" @click="() => redirectUris.push('')"
> >
<PlusIcon /> Add more <PlusIcon /> {{ formatMessage(messages.addMore) }}
</Button> </Button>
</div> </div>
</div> </div>
<div v-if="redirectUris.length <= 0"> <div v-if="redirectUris.length <= 0">
<Button color="primary" icon-only @click="() => redirectUris.push('')"> <Button color="primary" icon-only @click="() => redirectUris.push('')">
<PlusIcon /> Add a redirect uri <PlusIcon /> {{ formatMessage(messages.addRedirectUri) }}
</Button> </Button>
</div> </div>
</div> </div>
@@ -100,7 +118,7 @@
<div class="submit-row input-group push-right"> <div class="submit-row input-group push-right">
<button class="iconified-button" @click="$refs.appModal.hide()"> <button class="iconified-button" @click="$refs.appModal.hide()">
<XIcon /> <XIcon />
Cancel {{ formatMessage(messages.cancel) }}
</button> </button>
<button <button
v-if="editingId" v-if="editingId"
@@ -110,7 +128,7 @@
@click="editApp" @click="editApp"
> >
<SaveIcon /> <SaveIcon />
Save changes {{ formatMessage(messages.saveChanges) }}
</button> </button>
<button <button
v-else v-else
@@ -120,7 +138,7 @@
@click="createApp" @click="createApp"
> >
<PlusIcon /> <PlusIcon />
Create App {{ formatMessage(messages.createApp) }}
</button> </button>
</div> </div>
</div> </div>
@@ -144,13 +162,17 @@
} }
" "
> >
<PlusIcon /> New Application <PlusIcon /> {{ formatMessage(messages.newApplication) }}
</button> </button>
</div> </div>
<p> <p>
Applications can be used to authenticate Modrinth's users with your products. For more <IntlFormatted :message-id="messages.descriptionIntro">
information, see <template #docs-link="{ children }">
<a class="text-link" href="https://docs.modrinth.com">Modrinth's API documentation</a>. <a class="text-link" href="https://docs.modrinth.com">
<component :is="() => normalizeChildren(children)" />
</a>
</template>
</IntlFormatted>
</p> </p>
<div v-for="app in usersApps" :key="app.id" class="universal-card recessed token mt-4"> <div v-for="app in usersApps" :key="app.id" class="universal-card recessed token mt-4">
<div class="token-info"> <div class="token-info">
@@ -158,25 +180,31 @@
<Avatar size="sm" :src="app.icon_url" /> <Avatar size="sm" :src="app.icon_url" />
<div> <div>
<h2 class="token-title">{{ app.name }}</h2> <h2 class="token-title">{{ app.name }}</h2>
<div>Created on {{ new Date(app.created).toLocaleDateString() }}</div> <div>
{{
formatMessage(messages.createdOn, {
date: new Date(app.created).toLocaleDateString(),
})
}}
</div>
</div> </div>
</div> </div>
<div> <div>
<label for="token-information"> <label for="token-information">
<span class="label__title">About</span> <span class="label__title">{{ formatMessage(messages.aboutLabel) }}</span>
</label> </label>
<div class="token-content"> <div class="token-content">
<div> <div>
Client ID {{ formatMessage(messages.clientId) }}
<CopyCode :text="app.id" /> <CopyCode :text="app.id" />
</div> </div>
<div v-if="!!clientCreatedInState(app.id)"> <div v-if="!!clientCreatedInState(app.id)">
<div> <div>
Client Secret {{ formatMessage(messages.clientSecret) }}
<CopyCode :text="clientCreatedInState(app.id)?.client_secret" /> <CopyCode :text="clientCreatedInState(app.id)?.client_secret" />
</div> </div>
<div class="secret_disclaimer"> <div class="secret_disclaimer">
<i> Save your secret now, it will be hidden after you leave this page! </i> <i>{{ formatMessage(messages.secretDisclaimer) }}</i>
</div> </div>
</div> </div>
</div> </div>
@@ -196,7 +224,7 @@
" "
> >
<EditIcon /> <EditIcon />
Edit {{ formatMessage(messages.edit) }}
</Button> </Button>
<Button <Button
color="danger" color="danger"
@@ -209,7 +237,7 @@
" "
> >
<TrashIcon /> <TrashIcon />
Delete {{ formatMessage(messages.delete) }}
</Button> </Button>
</div> </div>
</div> </div>
@@ -224,8 +252,11 @@ import {
commonSettingsMessages, commonSettingsMessages,
ConfirmModal, ConfirmModal,
CopyCode, CopyCode,
defineMessages,
FileInput, FileInput,
injectNotificationManager, injectNotificationManager,
IntlFormatted,
normalizeChildren,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
@@ -233,6 +264,7 @@ import Modal from '~/components/ui/Modal.vue'
import { import {
getScopeValue, getScopeValue,
hasScope, hasScope,
scopeCategoryMessages,
scopeList, scopeList,
toggleScope, toggleScope,
useScopes, useScopes,
@@ -249,8 +281,196 @@ useHead({
title: 'Applications - Modrinth', title: 'Applications - Modrinth',
}) })
const messages = defineMessages({
modalHeader: {
id: 'settings.applications.modal.header',
defaultMessage: 'Application information',
},
deleteConfirmTitle: {
id: 'settings.applications.delete.confirm.title',
defaultMessage: 'Are you sure you want to delete this application?',
},
deleteConfirmDescription: {
id: 'settings.applications.delete.confirm.description',
defaultMessage:
'This will permanently delete this application and revoke all access tokens. (forever!)',
},
deleteConfirmButton: {
id: 'settings.applications.delete.confirm.button',
defaultMessage: 'Delete this application',
},
nameLabel: {
id: 'settings.applications.field.name',
defaultMessage: 'Name',
},
namePlaceholder: {
id: 'settings.applications.field.name.placeholder',
defaultMessage: "Enter the application's name...",
},
iconLabel: {
id: 'settings.applications.field.icon',
defaultMessage: 'Icon',
},
uploadIcon: {
id: 'settings.applications.button.upload-icon',
defaultMessage: 'Upload icon',
},
urlLabel: {
id: 'settings.applications.field.url',
defaultMessage: 'URL',
},
urlPlaceholder: {
id: 'settings.applications.field.url.placeholder',
defaultMessage: 'https://example.com',
},
descriptionLabel: {
id: 'settings.applications.field.description',
defaultMessage: 'Description',
},
descriptionPlaceholder: {
id: 'settings.applications.field.description.placeholder',
defaultMessage: "Enter the application's description...",
},
scopesLabel: {
id: 'settings.applications.field.scopes',
defaultMessage: 'Scopes',
},
redirectUrisLabel: {
id: 'settings.applications.field.redirect-uris',
defaultMessage: 'Redirect uris',
},
redirectUriPlaceholder: {
id: 'settings.applications.field.redirect-uri.placeholder',
defaultMessage: 'https://example.com/auth/callback',
},
addMore: {
id: 'settings.applications.button.add-more',
defaultMessage: 'Add more',
},
addRedirectUri: {
id: 'settings.applications.button.add-redirect-uri',
defaultMessage: 'Add a redirect uri',
},
cancel: {
id: 'settings.applications.button.cancel',
defaultMessage: 'Cancel',
},
saveChanges: {
id: 'settings.applications.button.save-changes',
defaultMessage: 'Save changes',
},
createApp: {
id: 'settings.applications.button.create',
defaultMessage: 'Create app',
},
newApplication: {
id: 'settings.applications.button.new',
defaultMessage: 'New application',
},
descriptionIntro: {
id: 'settings.applications.description.intro',
defaultMessage:
"Applications can be used to authenticate Modrinth's users with your products. For more information, see <docs-link>Modrinth's API documentation</docs-link>.",
},
aboutLabel: {
id: 'settings.applications.about',
defaultMessage: 'About',
},
clientId: {
id: 'settings.applications.client-id',
defaultMessage: 'Client ID',
},
clientSecret: {
id: 'settings.applications.client-secret',
defaultMessage: 'Client secret',
},
secretDisclaimer: {
id: 'settings.applications.secret.disclaimer',
defaultMessage: 'Save your secret now, it will be hidden after you leave this page!',
},
createdOn: {
id: 'settings.applications.created-on',
defaultMessage: 'Created on {date}',
},
edit: {
id: 'settings.applications.button.edit',
defaultMessage: 'Edit',
},
delete: {
id: 'settings.applications.button.delete',
defaultMessage: 'Delete',
},
iconUpdatedTitle: {
id: 'settings.applications.notification.icon-updated.title',
defaultMessage: 'Icon updated',
},
iconUpdatedDescription: {
id: 'settings.applications.notification.icon-updated.description',
defaultMessage: 'Your application icon has been updated.',
},
errorTitle: {
id: 'settings.applications.notification.error.title',
defaultMessage: 'An error occurred',
},
})
const { scopesToLabels } = useScopes() const { scopesToLabels } = useScopes()
const scopeCategories = computed(() => {
return [
{
name: formatMessage(scopeCategoryMessages.categoryUserAccount),
scopes: scopeList.filter((s) => s.startsWith('USER_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryProjects),
scopes: scopeList.filter((s) => s.startsWith('PROJECT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryVersions),
scopes: scopeList.filter((s) => s.startsWith('VERSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryCollections),
scopes: scopeList.filter((s) => s.startsWith('COLLECTION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryOrganizations),
scopes: scopeList.filter((s) => s.startsWith('ORGANIZATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryReports),
scopes: scopeList.filter((s) => s.startsWith('REPORT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryThreads),
scopes: scopeList.filter((s) => s.startsWith('THREAD_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPats),
scopes: scopeList.filter((s) => s.startsWith('PAT_')),
},
{
name: formatMessage(scopeCategoryMessages.categorySessions),
scopes: scopeList.filter((s) => s.startsWith('SESSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryNotifications),
scopes: scopeList.filter((s) => s.startsWith('NOTIFICATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPayouts),
scopes: scopeList.filter((s) => s.startsWith('PAYOUTS_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryAnalytics),
scopes: scopeList.filter(
(s) => s.startsWith('ANALYTICS') || s.startsWith('PERFORM_ANALYTICS'),
),
},
].filter((c) => c.scopes.length > 0)
})
const appModal = ref() const appModal = ref()
// Any apps created in the current state will be stored here // Any apps created in the current state will be stored here
@@ -347,8 +567,8 @@ async function onImageSelection(files) {
} }
addNotification({ addNotification({
title: 'Icon updated', title: formatMessage(messages.iconUpdatedTitle),
text: 'Your application icon has been updated.', text: formatMessage(messages.iconUpdatedDescription),
type: 'success', type: 'success',
}) })
} }
@@ -377,7 +597,7 @@ async function createApp() {
await refresh() await refresh()
} catch (err) { } catch (err) {
addNotification({ addNotification({
title: 'An error occurred', title: formatMessage(messages.errorTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: 'error',
}) })
@@ -445,7 +665,7 @@ async function editApp() {
appModal.value.hide() appModal.value.hide()
} catch (err) { } catch (err) {
addNotification({ addNotification({
title: 'An error occurred', title: formatMessage(messages.errorTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: 'error',
}) })
@@ -468,7 +688,7 @@ async function removeApp() {
editingId.value = null editingId.value = null
} catch (err) { } catch (err) {
addNotification({ addNotification({
title: 'An error occurred', title: formatMessage(messages.errorTitle),
text: err.data ? err.data.description : err, text: err.data ? err.data.description : err,
type: 'error', type: 'error',
}) })
@@ -500,17 +720,10 @@ async function removeApp() {
flex-basis: 24rem !important; flex-basis: 24rem !important;
} }
} }
.checkboxes {
display: grid;
column-gap: 0.5rem;
@media screen and (min-width: 432px) { .scope-items :deep(.checkbox-outer) {
grid-template-columns: repeat(2, 1fr); white-space: nowrap !important;
} justify-content: flex-start !important;
@media screen and (min-width: 800px) {
grid-template-columns: repeat(3, 1fr);
}
} }
.icon-submission { .icon-submission {

View File

@@ -29,16 +29,26 @@
<label for="pat-scopes"> <label for="pat-scopes">
<span class="label__title">{{ formatMessage(commonMessages.scopesLabel) }}</span> <span class="label__title">{{ formatMessage(commonMessages.scopesLabel) }}</span>
</label> </label>
<div id="pat-scopes" class="checkboxes"> <div
<Checkbox id="pat-scopes"
v-for="scope in scopeList" class="scope-items mt-2 grid grid-cols-1 gap-x-6 gap-y-4 min-[600px]:grid-cols-2"
:key="scope" >
:label="scopesToLabels(getScopeValue(scope)).join(', ')" <div v-for="category in scopeCategories" :key="category.name" class="flex flex-col gap-2">
:model-value="hasScope(scopesVal, scope)" <h4 class="m-0 border-b border-divider pb-1 text-base font-bold text-contrast">
@update:model-value="scopesVal = toggleScope(scopesVal, scope)" {{ category.name }}
/> </h4>
<div class="flex flex-col gap-2">
<Checkbox
v-for="scope in category.scopes"
:key="scope"
:label="scopesToLabels(getScopeValue(scope)).join(', ')"
:model-value="hasScope(scopesVal, scope)"
@update:model-value="scopesVal = toggleScope(scopesVal, scope)"
/>
</div>
</div>
</div> </div>
<label for="pat-name"> <label for="pat-name" class="mt-4">
<span class="label__title">{{ formatMessage(createModalMessages.expiresLabel) }}</span> <span class="label__title">{{ formatMessage(createModalMessages.expiresLabel) }}</span>
</label> </label>
<input id="pat-name" v-model="expires" type="date" /> <input id="pat-name" v-model="expires" type="date" />
@@ -171,7 +181,7 @@
</template> </template>
</div> </div>
</div> </div>
<div class="input-group"> <div class="token-actions ml-auto flex flex-col gap-2">
<button <button
class="iconified-button raised-button" class="iconified-button raised-button"
@click=" @click="
@@ -220,6 +230,7 @@ import Modal from '~/components/ui/Modal.vue'
import { import {
getScopeValue, getScopeValue,
hasScope, hasScope,
scopeCategoryMessages,
scopeList, scopeList,
toggleScope, toggleScope,
useScopes, useScopes,
@@ -338,6 +349,61 @@ const displayPats = computed(() => {
return pats.value.toSorted((a, b) => new Date(b.created) - new Date(a.created)) return pats.value.toSorted((a, b) => new Date(b.created) - new Date(a.created))
}) })
const scopeCategories = computed(() => {
return [
{
name: formatMessage(scopeCategoryMessages.categoryUserAccount),
scopes: scopeList.filter((s) => s.startsWith('USER_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryProjects),
scopes: scopeList.filter((s) => s.startsWith('PROJECT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryVersions),
scopes: scopeList.filter((s) => s.startsWith('VERSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryCollections),
scopes: scopeList.filter((s) => s.startsWith('COLLECTION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryOrganizations),
scopes: scopeList.filter((s) => s.startsWith('ORGANIZATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryReports),
scopes: scopeList.filter((s) => s.startsWith('REPORT_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryThreads),
scopes: scopeList.filter((s) => s.startsWith('THREAD_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPats),
scopes: scopeList.filter((s) => s.startsWith('PAT_')),
},
{
name: formatMessage(scopeCategoryMessages.categorySessions),
scopes: scopeList.filter((s) => s.startsWith('SESSION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryNotifications),
scopes: scopeList.filter((s) => s.startsWith('NOTIFICATION_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryPayouts),
scopes: scopeList.filter((s) => s.startsWith('PAYOUTS_')),
},
{
name: formatMessage(scopeCategoryMessages.categoryAnalytics),
scopes: scopeList.filter(
(s) => s.startsWith('ANALYTICS') || s.startsWith('PERFORM_ANALYTICS'),
),
},
].filter((c) => c.scopes.length > 0)
})
async function createPat() { async function createPat() {
startLoading() startLoading()
loading.value = true loading.value = true
@@ -407,17 +473,9 @@ async function removePat(id) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.checkboxes { .scope-items :deep(.checkbox-outer) {
display: grid; white-space: nowrap !important;
column-gap: 0.5rem; justify-content: flex-start !important;
@media screen and (min-width: 432px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (min-width: 800px) {
grid-template-columns: repeat(3, 1fr);
}
} }
.token { .token {
@@ -428,10 +486,6 @@ async function removePat(id) {
@media screen and (min-width: 800px) { @media screen and (min-width: 800px) {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.input-group {
margin-left: auto;
}
} }
} }
</style> </style>