* Improve Intl formatting * Additional fixes * Fixed formatters were not updated on locale change * Fixed formatNumber was not updated on locale change * Additional formatting and fixes after merge * Run prepr:frontend * Remove `'` in icon map * Run `pnpm install` * fix: lint + import * Additional fixes --------- Co-authored-by: Calum H. <calum@modrinth.com> Co-authored-by: Calum H. (IMB11) <contact@cal.engineer>
785 lines
19 KiB
Vue
785 lines
19 KiB
Vue
<template>
|
|
<div class="universal-card">
|
|
<ConfirmModal
|
|
ref="modal_confirm"
|
|
:title="formatMessage(messages.deleteConfirmTitle)"
|
|
:description="formatMessage(messages.deleteConfirmDescription)"
|
|
:proceed-label="formatMessage(messages.deleteConfirmButton)"
|
|
@proceed="removeApp(editingId)"
|
|
/>
|
|
<Modal ref="appModal" :header="formatMessage(messages.modalHeader)">
|
|
<div class="universal-modal">
|
|
<label for="app-name"
|
|
><span class="label__title">{{ formatMessage(messages.nameLabel) }}</span>
|
|
</label>
|
|
<StyledInput
|
|
id="app-name"
|
|
v-model="name"
|
|
:maxlength="2048"
|
|
autocomplete="off"
|
|
:placeholder="formatMessage(messages.namePlaceholder)"
|
|
/>
|
|
<label v-if="editingId" for="app-icon"
|
|
><span class="label__title">{{ formatMessage(messages.iconLabel) }}</span>
|
|
</label>
|
|
<div v-if="editingId" class="icon-submission">
|
|
<Avatar size="md" :src="icon" />
|
|
<FileInput
|
|
:max-size="262144"
|
|
class="btn"
|
|
:prompt="formatMessage(messages.uploadIcon)"
|
|
accept="image/png,image/jpeg,image/gif,image/webp"
|
|
@change="onImageSelection"
|
|
>
|
|
<UploadIcon />
|
|
</FileInput>
|
|
</div>
|
|
<label v-if="editingId" for="app-url">
|
|
<span class="label__title">{{ formatMessage(messages.urlLabel) }}</span>
|
|
</label>
|
|
<StyledInput
|
|
v-if="editingId"
|
|
id="app-url"
|
|
v-model="url"
|
|
:maxlength="255"
|
|
type="url"
|
|
autocomplete="off"
|
|
:placeholder="formatMessage(messages.urlPlaceholder)"
|
|
/>
|
|
<label v-if="editingId" for="app-description">
|
|
<span class="label__title">{{ formatMessage(messages.descriptionLabel) }}</span>
|
|
</label>
|
|
<StyledInput
|
|
v-if="editingId"
|
|
id="app-description"
|
|
v-model="description"
|
|
multiline
|
|
:maxlength="255"
|
|
autocomplete="off"
|
|
:placeholder="formatMessage(messages.descriptionPlaceholder)"
|
|
input-class="h-24 resize-y"
|
|
/>
|
|
<label for="app-scopes"
|
|
><span class="label__title">{{ formatMessage(messages.scopesLabel) }}</span>
|
|
</label>
|
|
<div
|
|
id="app-scopes"
|
|
class="scope-items mt-2 grid grid-cols-1 gap-x-6 gap-y-4 min-[600px]:grid-cols-2"
|
|
>
|
|
<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>
|
|
<label for="app-redirect-uris" class="mt-4"
|
|
><span class="label__title">{{ formatMessage(messages.redirectUrisLabel) }}</span>
|
|
</label>
|
|
<div class="uri-input-list">
|
|
<div v-for="(_, index) in redirectUris" :key="index">
|
|
<div class="input-group url-input-group-fixes">
|
|
<StyledInput
|
|
v-model="redirectUris[index]"
|
|
:maxlength="2048"
|
|
type="url"
|
|
autocomplete="off"
|
|
:placeholder="formatMessage(messages.redirectUriPlaceholder)"
|
|
/>
|
|
<Button v-if="index !== 0" icon-only @click="() => redirectUris.splice(index, 1)">
|
|
<TrashIcon />
|
|
</Button>
|
|
<Button
|
|
v-if="index === 0"
|
|
color="primary"
|
|
icon-only
|
|
@click="() => redirectUris.push('')"
|
|
>
|
|
<PlusIcon /> {{ formatMessage(messages.addMore) }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div v-if="redirectUris.length <= 0">
|
|
<Button color="primary" icon-only @click="() => redirectUris.push('')">
|
|
<PlusIcon /> {{ formatMessage(messages.addRedirectUri) }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="submit-row input-group push-right">
|
|
<button class="iconified-button" @click="$refs.appModal.hide()">
|
|
<XIcon />
|
|
{{ formatMessage(messages.cancel) }}
|
|
</button>
|
|
<button
|
|
v-if="editingId"
|
|
:disabled="!canSubmit"
|
|
type="button"
|
|
class="iconified-button brand-button"
|
|
@click="editApp"
|
|
>
|
|
<SaveIcon />
|
|
{{ formatMessage(messages.saveChanges) }}
|
|
</button>
|
|
<button
|
|
v-else
|
|
:disabled="!canSubmit"
|
|
type="button"
|
|
class="iconified-button brand-button"
|
|
@click="createApp"
|
|
>
|
|
<PlusIcon />
|
|
{{ formatMessage(messages.createApp) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
|
|
<div class="header__row">
|
|
<div class="header__title">
|
|
<h2 class="text-2xl">{{ formatMessage(commonSettingsMessages.applications) }}</h2>
|
|
</div>
|
|
<button
|
|
class="btn btn-primary"
|
|
@click="
|
|
() => {
|
|
name = null
|
|
icon = null
|
|
scopesVal = 0
|
|
redirectUris = ['']
|
|
editingId = null
|
|
expires = null
|
|
$refs.appModal.show()
|
|
}
|
|
"
|
|
>
|
|
<PlusIcon /> {{ formatMessage(messages.newApplication) }}
|
|
</button>
|
|
</div>
|
|
<p>
|
|
<IntlFormatted :message-id="messages.descriptionIntro">
|
|
<template #docs-link="{ children }">
|
|
<a class="text-link" href="https://docs.modrinth.com">
|
|
<component :is="() => normalizeChildren(children)" />
|
|
</a>
|
|
</template>
|
|
</IntlFormatted>
|
|
</p>
|
|
<div v-for="app in usersApps" :key="app.id" class="universal-card recessed token mt-4">
|
|
<div class="token-info">
|
|
<div class="token-icon">
|
|
<Avatar size="sm" :src="app.icon_url" />
|
|
<div>
|
|
<h2 class="token-title">{{ app.name }}</h2>
|
|
<div>
|
|
{{
|
|
formatMessage(messages.createdOn, {
|
|
date: formatDate(new Date(app.created)),
|
|
})
|
|
}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="token-information">
|
|
<span class="label__title">{{ formatMessage(messages.aboutLabel) }}</span>
|
|
</label>
|
|
<div class="token-content">
|
|
<div>
|
|
{{ formatMessage(messages.clientId) }}
|
|
<CopyCode :text="app.id" />
|
|
</div>
|
|
<div v-if="!!clientCreatedInState(app.id)">
|
|
<div>
|
|
{{ formatMessage(messages.clientSecret) }}
|
|
<CopyCode :text="clientCreatedInState(app.id)?.client_secret" />
|
|
</div>
|
|
<div class="secret_disclaimer">
|
|
<i>{{ formatMessage(messages.secretDisclaimer) }}</i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="input-group">
|
|
<Button
|
|
icon-only
|
|
@click="
|
|
() => {
|
|
setForm({
|
|
...app,
|
|
redirect_uris: app.redirect_uris.map((u) => u.uri) || [],
|
|
})
|
|
$refs.appModal.show()
|
|
}
|
|
"
|
|
>
|
|
<EditIcon />
|
|
{{ formatMessage(messages.edit) }}
|
|
</Button>
|
|
<Button
|
|
color="danger"
|
|
icon-only
|
|
@click="
|
|
() => {
|
|
editingId = app.id
|
|
$refs.modal_confirm.show()
|
|
}
|
|
"
|
|
>
|
|
<TrashIcon />
|
|
{{ formatMessage(messages.delete) }}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup>
|
|
import { EditIcon, PlusIcon, SaveIcon, TrashIcon, UploadIcon, XIcon } from '@modrinth/assets'
|
|
import {
|
|
Avatar,
|
|
Button,
|
|
Checkbox,
|
|
commonSettingsMessages,
|
|
ConfirmModal,
|
|
CopyCode,
|
|
defineMessages,
|
|
FileInput,
|
|
injectNotificationManager,
|
|
IntlFormatted,
|
|
normalizeChildren,
|
|
StyledInput,
|
|
useFormatDateTime,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
|
|
import Modal from '~/components/ui/Modal.vue'
|
|
import {
|
|
getScopeValue,
|
|
hasScope,
|
|
scopeCategoryMessages,
|
|
scopeList,
|
|
toggleScope,
|
|
useScopes,
|
|
} from '~/composables/auth/scopes.ts'
|
|
|
|
const { addNotification } = injectNotificationManager()
|
|
const { formatMessage } = useVIntl()
|
|
const formatDate = useFormatDateTime()
|
|
|
|
definePageMeta({
|
|
middleware: 'auth',
|
|
})
|
|
|
|
useHead({
|
|
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 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()
|
|
|
|
// Any apps created in the current state will be stored here
|
|
// Users can copy Client Secrets and such before the page reloads
|
|
const createdApps = ref([])
|
|
|
|
const editingId = ref(null)
|
|
const name = ref(null)
|
|
const icon = ref(null)
|
|
const scopesVal = ref(BigInt(0))
|
|
const redirectUris = ref([''])
|
|
const url = ref(null)
|
|
const description = ref(null)
|
|
|
|
const loading = ref(false)
|
|
|
|
const auth = await useAuth()
|
|
|
|
const { data: usersApps, refresh } = await useAsyncData(
|
|
'usersApps',
|
|
() =>
|
|
useBaseFetch(`user/${auth.value.user.id}/oauth_apps`, {
|
|
apiVersion: 3,
|
|
}),
|
|
{
|
|
watch: [auth],
|
|
},
|
|
)
|
|
|
|
const setForm = (app) => {
|
|
if (app?.id) {
|
|
editingId.value = app.id
|
|
} else {
|
|
editingId.value = null
|
|
}
|
|
name.value = app?.name || ''
|
|
icon.value = app?.icon_url || ''
|
|
scopesVal.value = app?.max_scopes || BigInt(0)
|
|
url.value = app?.url || ''
|
|
description.value = app?.description || ''
|
|
|
|
if (app?.redirect_uris) {
|
|
redirectUris.value = app.redirect_uris.map((uri) => uri?.uri || uri)
|
|
} else {
|
|
redirectUris.value = ['']
|
|
}
|
|
}
|
|
|
|
const canSubmit = computed(() => {
|
|
// Make sure name, scopes, and return uri are at least filled in
|
|
const filledIn =
|
|
name.value && name.value !== '' && name.value?.length > 2 && redirectUris.value.length > 0
|
|
// Make sure the redirect uris are either one empty string or all filled in with valid urls
|
|
const oneValid = redirectUris.value.length === 1 && redirectUris.value[0] === ''
|
|
let allValid
|
|
try {
|
|
allValid = redirectUris.value.every((uri) => {
|
|
const url = new URL(uri)
|
|
return !!url
|
|
})
|
|
} catch {
|
|
allValid = false
|
|
}
|
|
return filledIn && (oneValid || allValid)
|
|
})
|
|
|
|
const clientCreatedInState = (id) => {
|
|
return createdApps.value.find((app) => app.id === id)
|
|
}
|
|
|
|
async function onImageSelection(files) {
|
|
if (!editingId.value) {
|
|
throw new Error('No editing id')
|
|
}
|
|
|
|
if (files.length > 0) {
|
|
const file = files[0]
|
|
const extFromType = file.type.split('/')[1]
|
|
|
|
await useBaseFetch('oauth/app/' + editingId.value + '/icon', {
|
|
method: 'PATCH',
|
|
internal: true,
|
|
body: file,
|
|
query: {
|
|
ext: extFromType,
|
|
},
|
|
})
|
|
|
|
await refresh()
|
|
|
|
const app = usersApps.value.find((app) => app.id === editingId.value)
|
|
if (app) {
|
|
setForm(app)
|
|
}
|
|
|
|
addNotification({
|
|
title: formatMessage(messages.iconUpdatedTitle),
|
|
text: formatMessage(messages.iconUpdatedDescription),
|
|
type: 'success',
|
|
})
|
|
}
|
|
}
|
|
|
|
async function createApp() {
|
|
startLoading()
|
|
loading.value = true
|
|
try {
|
|
const createdAppInfo = await useBaseFetch('oauth/app', {
|
|
method: 'POST',
|
|
internal: true,
|
|
body: {
|
|
name: name.value,
|
|
icon_url: icon.value,
|
|
max_scopes: Number(scopesVal.value), // JS is 52 bit for ints so we're good for now
|
|
redirect_uris: redirectUris.value,
|
|
},
|
|
})
|
|
|
|
createdApps.value.push(createdAppInfo)
|
|
|
|
setForm(null)
|
|
appModal.value.hide()
|
|
|
|
await refresh()
|
|
} catch (err) {
|
|
addNotification({
|
|
title: formatMessage(messages.errorTitle),
|
|
text: err.data ? err.data.description : err,
|
|
type: 'error',
|
|
})
|
|
}
|
|
loading.value = false
|
|
stopLoading()
|
|
}
|
|
|
|
async function editApp() {
|
|
startLoading()
|
|
loading.value = true
|
|
try {
|
|
if (!editingId.value) {
|
|
throw new Error('No editing id')
|
|
}
|
|
|
|
// check if there's any difference between the current app and the one in the state
|
|
const app = usersApps.value.find((app) => app.id === editingId.value)
|
|
if (!app) {
|
|
throw new Error('No app found')
|
|
}
|
|
|
|
if (
|
|
app.name === name.value &&
|
|
app.icon_url === icon.value &&
|
|
app.max_scopes === scopesVal.value &&
|
|
app.redirect_uris === redirectUris.value &&
|
|
app.url === url.value &&
|
|
app.description === description.value
|
|
) {
|
|
setForm(null)
|
|
editingId.value = null
|
|
appModal.value.hide()
|
|
throw new Error('No changes detected')
|
|
}
|
|
|
|
const body = {
|
|
name: name.value,
|
|
max_scopes: Number(scopesVal.value), // JS is 52 bit for ints so we're good for now
|
|
redirect_uris: redirectUris.value,
|
|
}
|
|
|
|
if (url.value && url.value?.length > 0) {
|
|
body.url = url.value
|
|
}
|
|
|
|
if (description.value && description.value?.length > 0) {
|
|
body.description = description.value
|
|
}
|
|
|
|
if (icon.value && icon.value?.length > 0) {
|
|
body.icon_url = icon.value
|
|
}
|
|
|
|
await useBaseFetch('oauth/app/' + editingId.value, {
|
|
method: 'PATCH',
|
|
internal: true,
|
|
body,
|
|
})
|
|
|
|
await refresh()
|
|
setForm(null)
|
|
editingId.value = null
|
|
|
|
appModal.value.hide()
|
|
} catch (err) {
|
|
addNotification({
|
|
title: formatMessage(messages.errorTitle),
|
|
text: err.data ? err.data.description : err,
|
|
type: 'error',
|
|
})
|
|
}
|
|
loading.value = false
|
|
stopLoading()
|
|
}
|
|
|
|
async function removeApp() {
|
|
startLoading()
|
|
try {
|
|
if (!editingId.value) {
|
|
throw new Error('No editing id')
|
|
}
|
|
await useBaseFetch(`oauth/app/${editingId.value}`, {
|
|
internal: true,
|
|
method: 'DELETE',
|
|
})
|
|
await refresh()
|
|
editingId.value = null
|
|
} catch (err) {
|
|
addNotification({
|
|
title: formatMessage(messages.errorTitle),
|
|
text: err.data ? err.data.description : err,
|
|
type: 'error',
|
|
})
|
|
}
|
|
stopLoading()
|
|
}
|
|
</script>
|
|
<style lang="scss" scoped>
|
|
.secret_disclaimer {
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
.submit-row {
|
|
padding-top: var(--gap-lg);
|
|
}
|
|
.uri-input-list {
|
|
display: grid;
|
|
row-gap: 0.5rem;
|
|
}
|
|
.url-input-group-fixes {
|
|
width: 100%;
|
|
|
|
input {
|
|
width: 100% !important;
|
|
flex-basis: 24rem !important;
|
|
}
|
|
}
|
|
|
|
.scope-items :deep(.checkbox-outer) {
|
|
white-space: nowrap !important;
|
|
justify-content: flex-start !important;
|
|
}
|
|
|
|
.icon-submission {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: var(--gap-md);
|
|
}
|
|
|
|
.token {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: var(--gap-sm);
|
|
|
|
.token-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--gap-sm);
|
|
}
|
|
|
|
.token-content {
|
|
display: grid;
|
|
gap: var(--gap-xs);
|
|
}
|
|
|
|
.token-icon {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--gap-lg);
|
|
padding-bottom: var(--gap-sm);
|
|
}
|
|
|
|
.token-heading {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: var(--font-weight-bold);
|
|
color: var(--color-gray-700);
|
|
|
|
margin-top: var(--spacing-card-md);
|
|
margin-bottom: var(--spacing-card-sm);
|
|
}
|
|
|
|
.token-title {
|
|
margin-bottom: var(--spacing-card-xs);
|
|
}
|
|
|
|
.input-group {
|
|
margin-left: auto;
|
|
|
|
// For the children override the padding so that y padding is --gap-sm and x padding is --gap-lg
|
|
// Knossos global styling breaks everything
|
|
> * {
|
|
padding: var(--gap-sm) var(--gap-lg);
|
|
}
|
|
}
|
|
|
|
@media screen and (min-width: 800px) {
|
|
flex-direction: row;
|
|
}
|
|
}
|
|
</style>
|