Better metadata editing division #79 (#82)

* feat: new dropdown-based editor switching

* feat: tab based switching

* feat: add icon

* fix: lint

* chore: i18n translations

oh boy was this a 'chore'
This commit is contained in:
DecDuck
2025-06-05 14:53:19 +10:00
committed by GitHub
parent 681efe95af
commit 9e929ddf98
21 changed files with 1144 additions and 1226 deletions

View File

@ -4,14 +4,12 @@
<h2
class="mt-2 text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
>
Authentication
{{ $t("users.admin.authentication.title") }}
</h2>
<p
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
>
Drop supports a variety of "authentication mechanisms". As you enable or
disable them, they are shown on the sign in screen for users to select
from. Click the dot menu to configure the authentication mechanism.
{{ $t("users.admin.authentication.description") }}
</p>
</div>
<ul
@ -40,7 +38,9 @@
<MenuButton
class="-m-2.5 block p-2.5 text-zinc-400 hover:text-zinc-300 transition-colors duration-200"
>
<span class="sr-only">Open options</span>
<span class="sr-only">{{
$t("users.admin.authentication.srOpenOptions")
}}</span>
<EllipsisHorizontalIcon class="h-5 w-5" aria-hidden="true" />
</MenuButton>
<transition
@ -61,9 +61,8 @@
active ? 'bg-zinc-800 outline-none' : '',
'block px-3 py-1 text-sm/6 text-zinc-100 transition-colors duration-200',
]"
>Configure<span class="sr-only"
>, {{ authMech.name }}</span
></NuxtLink
>{{ $t("users.admin.authentication.configure")
}}<span class="sr-only">{{ authMech.name }}</span></NuxtLink
>
</MenuItem>
</MenuItems>
@ -72,7 +71,9 @@
</div>
<dl class="-my-3 divide-y divide-zinc-700 px-6 py-4 text-sm/6">
<div class="flex justify-between gap-x-4 py-3">
<dt class="text-zinc-400">Enabled</dt>
<dt class="text-zinc-400">
{{ $t("users.admin.authentication.enabledKey") }}
</dt>
<dd class="flex items-center">
<span
:class="[
@ -84,7 +85,11 @@
>
<CheckIcon v-if="authMech.enabled" class="w-4 h-4 mr-1" />
<XMarkIcon v-else class="w-4 h-4 mr-1" />
{{ authMech.enabled ? "Enabled" : "Disabled" }}
{{
authMech.enabled
? $t("users.admin.authentication.enabled")
: $t("users.admin.authentication.disabled")
}}
</span>
</dd>
</div>
@ -122,6 +127,8 @@ definePageMeta({
layout: "admin",
});
const { t } = useI18n();
const enabledMechanisms = await $dropFetch("/api/v1/admin/auth");
const authenticationMechanisms: Array<{
@ -133,13 +140,13 @@ const authenticationMechanisms: Array<{
settings?: { [key: string]: string | undefined } | undefined | boolean;
}> = [
{
name: "Simple (username/password)",
name: t("users.admin.authentication.simple"),
mec: "Simple" as AuthMec,
icon: IconsSimpleAuthenticationLogo,
route: "/admin/users/auth/simple",
},
{
name: "OpenID Connect",
name: t("users.admin.authentication.oidc"),
mec: "OpenID" as AuthMec,
icon: IconsSSOLogo,
},

View File

@ -4,15 +4,12 @@
<h2
class="mt-2 text-xl font-semibold tracking-tight text-zinc-100 sm:text-3xl"
>
Simple authentication
{{ $t("users.admin.simple.title") }}
</h2>
<p
class="mt-2 text-pretty text-sm font-medium text-zinc-400 sm:text-md/8"
>
Simple authentication uses a system of 'invitations' to create users.
You can create an invitation, and optionally specify a username or email
for the user, and then it will generate a magic URL that can be used to
create an account.
{{ $t("users.admin.simple.description") }}
</p>
</div>
@ -22,7 +19,9 @@
class="-mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap"
>
<div class="mt-2">
<h3 class="text-base font-semibold text-zinc-100">Invitations</h3>
<h3 class="text-base font-semibold text-zinc-100">
{{ $t("users.admin.simple.invitationTitle") }}
</h3>
</div>
<div class="ml-4 mt-2 shrink-0">
<button
@ -30,7 +29,7 @@
class="relative inline-flex items-center rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/25 active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
@click="() => (createModalOpen = true)"
>
Create invitation
{{ $t("users.admin.simple.createInvitation") }}
</button>
</div>
</div>
@ -54,9 +53,14 @@
</div>
<p class="mt-1 flex text-xs/5 text-gray-500">
{{ invitation.username ?? "No username enforced." }}
|
{{ invitation.email ?? "No email enforced." }}
{{
invitation.username ??
$t("users.admin.simple.noUsernameEnforced")
}}
{{ $t("common.divider") }}
{{
invitation.email ?? $t("users.admin.simple.noEmailEnforced")
}}
</p>
</div>
</div>
@ -64,14 +68,29 @@
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm/6 text-zinc-100">
{{
invitation.isAdmin ? "Admin invitation" : "User invitation"
invitation.isAdmin
? $t("users.admin.simple.adminInvitation")
: $t("users.admin.simple.userInvitation")
}}
</p>
<p class="mt-1 text-xs/5 text-gray-500">
Expires:
<time :datetime="invitation.expires">{{
new Date(invitation.expires).toLocaleString()
}}</time>
<p class="mt-1 text-sm text-gray-500">
<!-- forever is relative, right? -->
<i18n-t
v-if="
new Date(invitation.expires).getTime() - Date.now() <
3.156e12 // 100 years
"
keypath="users.admin.simple.expires"
tag="span"
scope="global"
>
<template #expiry>
<RelativeTime :date="invitation.expires" />
</template>
</i18n-t>
<span v-else>
{{ $t("users.admin.simple.neverExpires") }}
</span>
</p>
</div>
<button @click="() => deleteInvitation(invitation.id)">
@ -85,7 +104,7 @@
</ul>
<div v-if="invitations.length == 0" class="py-4 text-zinc-400 text-sm">
No invitations.
{{ $t("users.admin.simple.noInvitations") }}
</div>
</div>
@ -128,13 +147,11 @@
<DialogTitle
as="h3"
class="text-base font-semibold text-zinc-100"
>Invite user to Drop
>{{ $t("users.admin.simple.inviteTitle") }}
</DialogTitle>
<div class="mt-2">
<p class="text-sm text-zinc-400">
Drop will generate a URL that you can send to the
person you want to invite. You can optionally specify
a username or email for them to use.
{{ $t("users.admin.simple.inviteDescription") }}
</p>
</div>
</div>
@ -145,7 +162,9 @@
<label
for="username"
class="block text-sm font-medium leading-6 text-zinc-100"
>Username (optional)</label
>{{
$t("users.admin.simple.inviteUsernameLabel")
}}</label
>
<p
:class="[
@ -153,7 +172,7 @@
'block text-xs font-medium leading-6',
]"
>
Must be 5 or more characters
{{ $t("users.admin.simple.inviteUsernameFormat") }}
</p>
<div class="mt-2">
<input
@ -162,7 +181,9 @@
name="invite-username"
type="text"
autocomplete="username"
placeholder="myUsername"
:placeholder="
$t('users.admin.simple.inviteUsernamePlaceholder')
"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
@ -172,7 +193,7 @@
<label
for="email"
class="block text-sm font-medium leading-6 text-zinc-100"
>Email address (optional)</label
>{{ $t("users.admin.simple.inviteEmailLabel") }}</label
>
<p
:class="[
@ -180,7 +201,7 @@
'block text-xs font-medium leading-6',
]"
>
Must be in the format user@example.com
{{ $t("users.admin.simple.inviteEmailDescription") }}
</p>
<div class="mt-2">
<input
@ -189,7 +210,9 @@
name="invite-email"
type="email"
autocomplete="email"
placeholder="me@example.com"
:placeholder="
$t('users.admin.simple.inviteEmailPlaceholder')
"
class="block w-full rounded-md border-0 py-1.5 px-3 bg-zinc-800 disabled:bg-zinc-900/80 text-zinc-100 disabled:text-zinc-400 shadow-sm ring-1 ring-inset ring-zinc-700 disabled:ring-zinc-800 placeholder:text-zinc-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6"
/>
</div>
@ -205,13 +228,18 @@
as="span"
class="text-sm/6 font-medium text-zinc-100"
passive
>Admin invitation
>{{
$t("users.admin.simple.inviteAdminSwitchLabel")
}}
</SwitchLabel>
<SwitchDescription
as="span"
class="text-sm text-zinc-400"
>Create this user as an
administrator</SwitchDescription
>{{
$t(
"users.admin.simple.inviteAdminSwitchDescription",
)
}}</SwitchDescription
>
</span>
<Switch
@ -236,7 +264,9 @@
<Listbox v-model="expiryKey" as="div">
<ListboxLabel
class="block text-sm/6 font-medium text-zinc-100"
>Expires in</ListboxLabel
>{{
$t("users.admin.simple.inviteExpiryLabel")
}}</ListboxLabel
>
<div class="relative mt-2">
<ListboxButton
@ -331,7 +361,7 @@
type="submit"
class="w-full sm:w-fit"
>
Invite
{{ $t("users.admin.simple.inviteButton") }}
</LoadingButton>
<button
ref="cancelButtonRef"
@ -339,7 +369,7 @@
class="mt-3 inline-flex w-full justify-center rounded-md bg-zinc-800 px-3 py-2 text-sm font-semibold text-zinc-100 shadow-sm ring-1 ring-inset ring-zinc-700 hover:bg-zinc-900 sm:mt-0 sm:w-auto"
@click="createModalOpen = false"
>
Cancel
{{ $t("cancel") }}
</button>
</div>
</form>
@ -374,6 +404,8 @@ import type { SerializeObject } from "nitropack";
import type { DurationLike } from "luxon";
import { DateTime } from "luxon";
const { t } = useI18n();
definePageMeta({
layout: "admin",
});
@ -431,23 +463,23 @@ const isAdmin = ref(false);
// Label to parameters to moment.js .add()
const expiry: Record<string, DurationLike> = {
"3 days": {
[t("users.admin.simple.invite3Days")]: {
days: 3,
},
"7 days": {
[t("users.admin.simple.inviteWeek")]: {
days: 7,
},
"1 month": {
[t("users.admin.simple.inviteMonth")]: {
month: 1,
},
"6 months": {
[t("users.admin.simple.invite6Months")]: {
months: 6,
},
"1 year": {
[t("users.admin.simple.inviteYear")]: {
year: 1,
},
Never: {
year: 3000,
[t("users.admin.simple.inviteNever")]: {
year: 5000,
}, // Never is relative, right?
};
const expiryKey = ref<keyof typeof expiry>(Object.keys(expiry)[0]); // Cast to any because we just know it's okay
@ -485,7 +517,7 @@ function invite_wrapper() {
invitationUrls.value?.push(generateInvitationUrl(invitation.id));
})
.catch((response) => {
const message = response.statusMessage || "An unknown error occurred";
const message = response.statusMessage || t("errors.unknown");
error.value = message;
})
.finally(() => {

View File

@ -6,8 +6,7 @@
{{ $t("header.admin.users") }}
</h1>
<p class="mt-2 text-sm text-zinc-400">
Manage the users on your Drop instance, and configure your
authentication methods.
{{ $t("users.admin.description") }}
</p>
</div>
<div class="mt-4 sm:ml-16 sm:mt-0 sm:flex-none">
@ -15,7 +14,11 @@
to="/admin/users/auth"
class="block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-500 hover:scale-105 hover:shadow-lg active:scale-95 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Authentication &rarr;
<i18n-t keypath="users.admin.authLink" tag="span" scope="global">
<template #arrow>
<span aria-hidden="true">{{ $t("chars.arrow") }}</span>
</template>
</i18n-t>
</NuxtLink>
</div>
</div>
@ -32,34 +35,36 @@
scope="col"
class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-zinc-100 sm:pl-6"
>
Display Name
{{ $t("users.admin.displayNameHeader") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Username
{{ $t("users.admin.usernameHeader") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Email
{{ $t("users.admin.emailHeader") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Admin?
{{ $t("users.admin.adminHeader") }}
</th>
<th
scope="col"
class="px-3 py-3.5 text-left text-sm font-semibold text-zinc-100"
>
Auth Options
{{ $t("users.admin.authoptionsHeader") }}
</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-6">
<span class="sr-only">Edit</span>
<span class="sr-only">
{{ $t("users.admin.srEditLabel") }}
</span>
</th>
</tr>
</thead>
@ -89,7 +94,11 @@
: 'bg-zinc-400/10 text-zinc-400 ring-zinc-400/20',
]"
>
{{ user.admin ? "Admin User" : "Normal user" }}
{{
user.admin
? $t("users.admin.adminUserLabel")
: $t("users.admin.normalUserLabel")
}}
</span>
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-zinc-400">