Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/app-frontend/src/pages/hosting/manage/Access.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ServersManageAccessPage,
} from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener'

const client = injectModrinthClient()
const { serverId } = injectModrinthServerContext()
Expand All @@ -26,8 +27,12 @@ try {
} catch {
// Let mounted layouts' useQuery surface errors; do not fail route setup.
}

function userProfileLink(username: string) {
return () => openUrl(`https://modrinth.com/user/${encodeURIComponent(username)}`)
}
</script>

<template>
<ServersManageAccessPage />
<ServersManageAccessPage :user-profile-link="userProfileLink" />
</template>
18 changes: 14 additions & 4 deletions packages/api-client/src/modules/archon/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,29 +858,39 @@ export namespace Archon {
id: string
}

export type UserInfo = {
id: string
username: string
avatar_url: string | null
}

export type DeleteManyBackupRequest = {
backup_ids: string[]
}

export type ActiveOperation = {
backup_id: string
operation_type: BackupQueueOperationType
operation_id?: number | null
operation_id: number | null
has_parent: boolean
scheduled_for: string
started_at: string | null
synthetic_legacy: boolean
user_info: UserInfo | null
}

export type BackupQueueOperation = {
operation_type: BackupQueueOperationType
operation_id?: number | null
operation_id: number | null
state: BackupQueueState
scheduled_for: string
completed_at?: string | null
started_at: string | null
completed_at: string | null
has_parent: boolean
error?: string | null
error: string | null
should_prompt: boolean
synthetic_legacy: boolean
user_info: UserInfo | null
}

export type BackupQueueBackup = {
Expand Down
12 changes: 9 additions & 3 deletions packages/ui/src/components/base/Combobox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
ref="searchTriggerRef"
v-model="searchQuery"
:icon="showSearchIcon ? SearchIcon : undefined"
type="text"
:type="searchType"
:name="searchName"
:placeholder="searchPlaceholder || placeholder"
:disabled="disabled"
:autocomplete="searchAutocomplete"
:autocorrect="searchAutocorrect"
:autocapitalize="searchAutocapitalize"
:spellcheck="searchSpellcheck"
:inputmode="searchInputmode"
:input-attrs="searchInputAttrs"
wrapper-class="w-full !bg-transparent"
:input-class="searchableInputClass"
class="relative z-[1]"
Expand Down Expand Up @@ -281,19 +284,21 @@ const props = withDefaults(
forceDirection?: 'up' | 'down'
noOptionsMessage?: string
disableSearchFilter?: boolean
dropdownClass?: string
dropdownMinWidth?: string
minSearchLengthToOpen?: number
/** Keep the selected option's label in the input after selection, and show all options on focus */
syncWithSelection?: boolean
/** Select the searchable input text when the field receives focus */
selectSearchTextOnFocus?: boolean
/** Show a search icon in the searchable input */
showSearchIcon?: boolean
searchType?: 'text' | 'search'
searchName?: string
searchInputmode?: 'text' | 'search'
searchAutocomplete?: string
searchAutocorrect?: 'on' | 'off'
searchAutocapitalize?: 'none' | 'off' | 'sentences' | 'words' | 'characters'
searchSpellcheck?: boolean
searchInputAttrs?: Record<string, string | number | boolean | undefined>
}>(),
{
placeholder: 'Select an option',
Expand All @@ -309,6 +314,7 @@ const props = withDefaults(
syncWithSelection: true,
selectSearchTextOnFocus: false,
showSearchIcon: false,
searchType: 'text',
outsideClickIgnore: () => [],
},
)
Expand Down
10 changes: 8 additions & 2 deletions packages/ui/src/components/base/DropdownFilterBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,13 @@
>
<div
v-if="isDropdownFilterSectionHeader(item)"
class="flex items-center justify-between gap-3 px-4 py-2.5 text-sm font-semibold text-secondary"
:class="item.class"
class="flex items-center justify-between gap-3 border-0 px-4 py-2.5 text-sm font-semibold text-secondary"
:class="[
item.class,
item.dividerBefore && index > 0
? 'border-t border-solid border-surface-5'
: undefined,
]"
>
<span class="flex min-w-0 items-center gap-2">
<component
Expand Down Expand Up @@ -398,6 +403,7 @@ export type DropdownFilterBarSectionHeader = {
key?: string
icon?: Component
class?: string
dividerBefore?: boolean
}

export type DropdownFilterBarItem = DropdownFilterBarOption | DropdownFilterBarSectionHeader
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/src/components/base/StyledInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<textarea
v-if="multiline"
:id="id"
v-bind="inputAttrs"
ref="inputRef"
:value="model"
:placeholder="placeholder"
Expand Down Expand Up @@ -50,6 +51,7 @@
<input
v-else
:id="id"
v-bind="inputAttrs"
ref="inputRef"
:type="type"
:value="model"
Expand Down Expand Up @@ -149,6 +151,7 @@ const props = withDefaults(
resize?: 'none' | 'vertical' | 'both'
inputClass?: string
wrapperClass?: string
inputAttrs?: Record<string, string | number | boolean | undefined>
}>(),
{
type: 'text',
Expand Down
18 changes: 9 additions & 9 deletions packages/ui/src/components/base/Table.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<div class="overflow-hidden rounded-2xl border border-solid border-surface-5">
<div class="overflow-hidden rounded-2xl border border-solid border-surface-4">
<div
v-if="hasHeaderSlot"
class="border-solid border-0 border-b border-surface-5 bg-surface-3 p-4"
class="border-solid border-0 border-b border-surface-4 bg-surface-3 p-4"
>
<slot name="header" />
</div>
<div class="overflow-x-auto overflow-y-hidden">
<table
class="w-full table-fixed border-separate border-spacing-0 border-surface-5"
class="w-full table-fixed border-separate border-spacing-0 border-surface-4"
:style="tableMinWidth ? { minWidth: tableMinWidth } : undefined"
>
<colgroup>
Expand Down Expand Up @@ -68,7 +68,7 @@
tag="tbody"
>
<tr v-if="data.length === 0" key="empty" class="bg-surface-2">
<td :colspan="columnSpan" class="border-solid border-0 border-t border-surface-5 p-0">
<td :colspan="columnSpan" class="border-solid border-0 border-t border-surface-4 p-0">
<slot name="empty-state">
<div class="text-secondary flex h-64 items-center justify-center">
No data available.
Expand All @@ -84,7 +84,7 @@
>
<td
v-if="showSelection"
class="w-12 border-solid border-0 border-t border-surface-5 focus:outline-none"
class="w-12 border-solid border-0 border-t border-surface-4 focus:outline-none"
>
<Checkbox
:model-value="isSelected(row)"
Expand All @@ -95,7 +95,7 @@
<td
v-for="column in columns"
:key="column.key"
class="text-secondary h-14 overflow-hidden first:pl-4 last:pr-4 border-solid border-0 border-t border-surface-5"
class="text-secondary h-14 overflow-hidden first:pl-4 last:pr-4 border-solid border-0 border-t border-surface-4"
:class="`text-${column.align ?? 'left'}`"
>
<slot
Expand All @@ -113,7 +113,7 @@
</TransitionGroup>
<tbody v-else :ref="setListContainer">
<tr v-if="data.length === 0" class="bg-surface-2">
<td :colspan="columnSpan" class="border-solid border-0 border-t border-surface-5 p-0">
<td :colspan="columnSpan" class="border-solid border-0 border-t border-surface-4 p-0">
<slot name="empty-state">
<div class="text-secondary flex h-64 items-center justify-center">
No data available.
Expand All @@ -136,7 +136,7 @@
>
<td
v-if="showSelection"
class="w-12 border-solid border-0 border-t border-surface-5 focus:outline-none"
class="w-12 border-solid border-0 border-t border-surface-4 focus:outline-none"
>
<Checkbox
:model-value="isSelected(row)"
Expand All @@ -147,7 +147,7 @@
<td
v-for="column in columns"
:key="column.key"
class="text-secondary h-14 overflow-hidden first:pl-4 last:pr-4 border-solid border-0 border-t border-surface-5"
class="text-secondary h-14 overflow-hidden first:pl-4 last:pr-4 border-solid border-0 border-t border-surface-4"
:class="`text-${column.align ?? 'left'}`"
>
<slot
Expand Down
43 changes: 28 additions & 15 deletions packages/ui/src/components/servers/access/AccessTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
>
<template #cell-user="{ row: member }">
<AutoLink
:to="userProfilePath(member.user.username)"
:to="getUserProfileLink(member.user.username)"
:target="userProfileTarget(member.user.username)"
class="inline-flex max-w-full min-w-0 items-center gap-2"
:class="userProfilePath(member.user.username) ? 'text-primary hover:underline' : ''"
:class="getUserProfileLink(member.user.username) ? 'text-primary hover:underline' : ''"
>
<Avatar
:src="member.user.avatarUrl"
Expand Down Expand Up @@ -61,7 +62,7 @@
<template #cell-joined="{ row: member }">
<span
v-if="member.pending"
class="inline-flex h-7 items-center rounded-full border border-surface-5 border-solid bg-surface-4 px-2.5 py-1 text-sm font-semibold text-secondary"
class="inline-flex h-7 items-center rounded-full border border-surface-4 border-solid bg-surface-4 px-2.5 py-1 text-sm font-semibold text-secondary"
>
{{ formatMessage(messages.pendingLabel) }}
</span>
Expand Down Expand Up @@ -102,7 +103,7 @@

<div
v-if="members.length > 0"
class="overflow-hidden rounded-2xl border border-solid border-surface-5 sm:hidden"
class="overflow-hidden rounded-2xl border border-solid border-surface-4 sm:hidden"
>
<div
class="grid min-h-14 grid-cols-[minmax(0,1.35fr)_7.75rem_minmax(6rem,0.8fr)_4rem] bg-surface-3"
Expand Down Expand Up @@ -147,15 +148,16 @@
<div
v-for="(member, index) in sortedMembers"
:key="member.id"
class="grid min-h-16 grid-cols-[minmax(0,1.35fr)_7.75rem_minmax(6rem,0.8fr)_4rem] items-center border-0 border-t border-solid border-surface-5"
class="grid min-h-16 grid-cols-[minmax(0,1.35fr)_7.75rem_minmax(6rem,0.8fr)_4rem] items-center border-0 border-t border-solid border-surface-4"
:class="index % 2 === 0 ? 'bg-surface-2' : 'bg-surface-1.5'"
>
<div class="flex min-w-0 items-center pl-4">
<AutoLink
v-tooltip="member.user.username"
:to="userProfilePath(member.user.username)"
:to="getUserProfileLink(member.user.username)"
:target="userProfileTarget(member.user.username)"
class="inline-flex min-w-0 items-center gap-2"
:class="userProfilePath(member.user.username) ? 'text-primary hover:underline' : ''"
:class="getUserProfileLink(member.user.username) ? 'text-primary hover:underline' : ''"
>
<Avatar
:src="member.user.avatarUrl"
Expand Down Expand Up @@ -207,7 +209,7 @@
<div class="min-w-0 py-3 pr-2 text-right text-secondary">
<span
v-if="member.pending"
class="inline-flex h-7 max-w-full items-center rounded-full border border-surface-5 border-solid bg-surface-4 px-2.5 py-1 text-sm font-semibold text-secondary"
class="inline-flex h-7 max-w-full items-center rounded-full border border-surface-4 border-solid bg-surface-4 px-2.5 py-1 text-sm font-semibold text-secondary"
>
{{ formatMessage(messages.pendingLabel) }}
</span>
Expand Down Expand Up @@ -248,7 +250,7 @@
</div>
</div>

<div v-else class="overflow-hidden rounded-2xl border border-solid border-surface-5">
<div v-else class="overflow-hidden rounded-2xl border border-solid border-surface-4">
<div
class="grid min-h-14 grid-cols-[3.75rem_7.25rem_minmax(0,1fr)_2.75rem] bg-surface-3 sm:h-14 sm:grid-cols-[32%_28%_28%_12%]"
>
Expand All @@ -266,7 +268,7 @@
</div>
</div>
<div
class="border-0 border-t border-solid border-surface-5 bg-surface-2 px-4 py-8 text-center text-secondary"
class="border-0 border-t border-solid border-surface-4 bg-surface-2 px-4 py-8 text-center text-secondary"
>
{{ formatMessage(messages.emptyState) }}
</div>
Expand All @@ -293,14 +295,20 @@ import ButtonStyled from '../../base/ButtonStyled.vue'
import Combobox, { type ComboboxOption } from '../../base/Combobox.vue'
import Table, { type SortDirection, type TableColumn } from '../../base/Table.vue'
import TeleportOverflowMenu from '../../base/TeleportOverflowMenu.vue'
import type { ServerAccessMember, ServerAccessRole, ServerAccessRoleOption } from './types'
import type {
ServerAccessMember,
ServerAccessRole,
ServerAccessRoleOption,
ServerAccessUserProfileLink,
} from './types'

const props = withDefaults(
defineProps<{
members: ServerAccessMember[]
roles: ServerAccessRoleOption[]
canManageUsers?: boolean
permissionDeniedMessage?: string
userProfileLink?: (username: string) => ServerAccessUserProfileLink
}>(),
{
canManageUsers: true,
Expand Down Expand Up @@ -353,11 +361,11 @@ const messages = defineMessages({
},
cancelInvite: {
id: 'servers.access-table.action.cancel-invite',
defaultMessage: 'Cancel invite',
defaultMessage: 'Revoke invite',
},
removeUser: {
id: 'servers.access-table.action.remove-user',
defaultMessage: 'Remove user',
defaultMessage: 'Revoke access',
},
emptyState: {
id: 'servers.access-table.empty',
Expand Down Expand Up @@ -533,9 +541,14 @@ function roleTriggerClass(role: ServerAccessRole): string {
return roleClasses(role)
}

function userProfilePath(username: string): string | undefined {
function getUserProfileLink(username: string): ServerAccessUserProfileLink {
if (!username || username.includes('@')) return undefined
return `/user/${encodeURIComponent(username)}`
return props.userProfileLink?.(username) ?? `/user/${encodeURIComponent(username)}`
}

function userProfileTarget(username: string): string | undefined {
const link = getUserProfileLink(username)
return typeof link === 'string' && link.startsWith('http') ? '_blank' : undefined
}

function resendInviteCooldownSeconds(member: ServerAccessMember): number {
Expand Down
Loading
Loading