Merge widget Workshop identity fix into mobile
This commit is contained in:
19 files changed
+114
-362
No files matched your search
@@ -95,8 +95,11 @@ irreversible development reset.
|
||||
GitHub needs only the central callback `https://<workshop-host>/api/oauth2-redirect`. Keep PocketBase origins unrestricted
|
||||
unless every self-hosted Homarr caller is explicitly listed. Configure both `GITHUB_CLIENT_ID` and
|
||||
`GITHUB_CLIENT_SECRET`; supplying only one is a startup error. Set `WORKSHOP_PUBLIC_ORIGIN` in production so generated
|
||||
links and email use the public host. Workshop uses the OAuth provider username as the public display name instead of
|
||||
the profile's full name. Configure SMTP for comment, report, and removal notices.
|
||||
links and email use the public host. Workshop stores only the OAuth provider username in `users.githubUsername`, and
|
||||
users cannot rename it. Map **OAuth2 username** to `githubUsername`; leave OAuth2 id, full name, and avatar mappings
|
||||
empty. Profile and avatar URLs are derived from `https://github.com/<username>` and
|
||||
`https://github.com/<username>.png`, so PocketBase does not store duplicate profile metadata or avatar files. Configure
|
||||
SMTP for comment, report, and removal notices.
|
||||
|
||||
The public URL contract is:
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { IconCheck, IconEdit, IconMessageCircle, IconRefresh, IconSend, IconTras
|
||||
|
||||
import type { WorkshopComment } from "@site/src/lib/pocketbase";
|
||||
import { getWorkshopBackend } from "@site/src/lib/pocketbase";
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
import type { WorkshopUser } from "@homarr/workshop/schema";
|
||||
import { errorMessage } from "@site/src/lib/utils";
|
||||
|
||||
@@ -158,13 +159,12 @@ export const CommentsSection = ({ submissionId, backend, currentUser, onRequireA
|
||||
const canDelete = ownComment || currentUser?.isAdmin === true;
|
||||
const isEditing = editingId === comment.id;
|
||||
const edited = comment.updated !== comment.created;
|
||||
const authorName = ownComment ? (currentUser?.displayName ?? comment.authorName) : comment.authorName;
|
||||
const avatarUrl = ownComment
|
||||
? (currentUser?.avatarUrl ?? comment.authorAvatarUrl)
|
||||
: comment.authorAvatarUrl;
|
||||
const profileUrl = ownComment
|
||||
? (currentUser?.githubProfileUrl ?? comment.authorGithubProfileUrl)
|
||||
: comment.authorGithubProfileUrl;
|
||||
const authorGithubUsername = ownComment
|
||||
? (currentUser?.githubUsername ?? comment.authorGithubUsername)
|
||||
: comment.authorGithubUsername;
|
||||
const authorName = authorGithubUsername || "Community member";
|
||||
const avatarUrl = githubAvatarUrl(authorGithubUsername);
|
||||
const profileUrl = githubProfileUrl(authorGithubUsername);
|
||||
|
||||
return (
|
||||
<article
|
||||
@@ -292,9 +292,9 @@ export const CommentsSection = ({ submissionId, backend, currentUser, onRequireA
|
||||
<div className="rounded-xl border border-border bg-muted/30 p-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<Avatar className="mt-0.5 size-9 bg-background ring-1 ring-border">
|
||||
{currentUser?.avatarUrl && <AvatarImage src={currentUser.avatarUrl} alt="" />}
|
||||
{currentUser?.githubUsername && <AvatarImage src={githubAvatarUrl(currentUser.githubUsername)} alt="" />}
|
||||
<AvatarFallback className="bg-background">
|
||||
{avatarFallback(currentUser?.displayName ?? "Guest")}
|
||||
{avatarFallback(currentUser?.githubUsername ?? "Guest")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<Textarea
|
||||
|
||||
@@ -33,6 +33,8 @@ import { getRuntimeWorkshopApiUrl } from "@site/src/lib/runtime-config";
|
||||
import type { SubmissionType } from "@site/src/lib/workshop-schema";
|
||||
import type { HomarrCustomWidgetV2 } from "@homarr/custom-widgets/core";
|
||||
import {
|
||||
githubAvatarUrl,
|
||||
githubProfileUrl,
|
||||
MAX_WORKSHOP_SCREENSHOTS,
|
||||
MAX_WORKSHOP_SCREENSHOT_BYTES,
|
||||
WORKSHOP_SCREENSHOT_MIME_TYPES,
|
||||
@@ -524,10 +526,10 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
const canManage = user?.id === submission.author || user?.isAdmin === true;
|
||||
const canEdit = user?.id === submission.author;
|
||||
const signedInAuthor = user?.id === submission.author ? user : null;
|
||||
const authorName = signedInAuthor?.displayName || submission.authorName;
|
||||
const authorAvatarUrl = signedInAuthor?.avatarUrl || submission.authorAvatarUrl;
|
||||
const authorGithubUsername = signedInAuthor?.githubUsername || submission.authorGithubUsername;
|
||||
const authorGithubProfileUrl = signedInAuthor?.githubProfileUrl || submission.authorGithubProfileUrl;
|
||||
const authorName = authorGithubUsername || "Community member";
|
||||
const authorAvatarUrl = githubAvatarUrl(authorGithubUsername);
|
||||
const authorGithubProfileUrl = githubProfileUrl(authorGithubUsername);
|
||||
const sources = widgetDefinition ? Object.entries(widgetDefinition.sources) : [];
|
||||
const requests = widgetDefinition ? Object.entries(widgetDefinition.requests) : [];
|
||||
const options = widgetDefinition ? Object.entries(widgetDefinition.options) : [];
|
||||
@@ -593,10 +595,7 @@ const MarketplaceDetail = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
{authorName}
|
||||
{authorGithubProfileUrl && <IconBrandGithub size={14} />}
|
||||
</a>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{authorGithubUsername ? `@${authorGithubUsername} · ` : ""}
|
||||
Published {formatRelativeTime(submission.created)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Published {formatRelativeTime(submission.created)}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { IconBrandGithub, IconChevronDown, IconExternalLink, IconLogout } from "@tabler/icons-react";
|
||||
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
import type { WorkshopUser } from "@homarr/workshop/schema";
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
@@ -9,10 +10,6 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
const avatarFallback = (name: string) => name.trim().slice(0, 1).toUpperCase() || "?";
|
||||
|
||||
const githubProfileUrl = (user: WorkshopUser) =>
|
||||
user.githubProfileUrl ||
|
||||
(user.githubUsername ? `https://github.com/${encodeURIComponent(user.githubUsername)}` : undefined);
|
||||
|
||||
interface WorkshopAccountMenuProps {
|
||||
user: WorkshopUser;
|
||||
onSignOut: () => void;
|
||||
@@ -21,8 +18,9 @@ interface WorkshopAccountMenuProps {
|
||||
export const WorkshopAccountMenu = ({ user, onSignOut }: WorkshopAccountMenuProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const profileUrl = githubProfileUrl(user);
|
||||
const accountLabel = user.githubUsername ? `@${user.githubUsername}` : user.displayName || "Account";
|
||||
const profileUrl = githubProfileUrl(user.githubUsername);
|
||||
const avatarUrl = githubAvatarUrl(user.githubUsername);
|
||||
const accountLabel = user.githubUsername ? `@${user.githubUsername}` : "Account";
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -54,8 +52,8 @@ export const WorkshopAccountMenu = ({ user, onSignOut }: WorkshopAccountMenuProp
|
||||
onClick={() => setOpen((value) => !value)}
|
||||
>
|
||||
<Avatar className="size-6">
|
||||
{user.avatarUrl && <AvatarImage src={user.avatarUrl} alt="" />}
|
||||
<AvatarFallback className="text-[10px]">{avatarFallback(user.displayName)}</AvatarFallback>
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt="" />}
|
||||
<AvatarFallback className="text-[10px]">{avatarFallback(user.githubUsername)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="max-w-36 truncate">{accountLabel}</span>
|
||||
<IconChevronDown
|
||||
@@ -73,12 +71,11 @@ export const WorkshopAccountMenu = ({ user, onSignOut }: WorkshopAccountMenuProp
|
||||
>
|
||||
<div className="flex items-center gap-3 px-2 py-2">
|
||||
<Avatar className="size-9">
|
||||
{user.avatarUrl && <AvatarImage src={user.avatarUrl} alt="" />}
|
||||
<AvatarFallback>{avatarFallback(user.displayName)}</AvatarFallback>
|
||||
{avatarUrl && <AvatarImage src={avatarUrl} alt="" />}
|
||||
<AvatarFallback>{avatarFallback(user.githubUsername)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{user.displayName}</p>
|
||||
{user.githubUsername && <p className="truncate text-xs text-muted-foreground">@{user.githubUsername}</p>}
|
||||
<p className="truncate text-sm font-medium">{accountLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "@tabler/icons-react";
|
||||
|
||||
import { WorkshopBackend } from "@homarr/workshop/backend";
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
import type { WorkshopReport, WorkshopSubmissionSummary, WorkshopUser } from "@homarr/workshop/schema";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
@@ -194,18 +195,20 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) {
|
||||
<CardHeader className="border-b border-border pb-4 sm:grid-cols-[1fr_auto]">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Avatar className="size-10">
|
||||
{submission.authorAvatarUrl && <AvatarImage src={submission.authorAvatarUrl} alt="" />}
|
||||
<AvatarFallback>{avatarFallback(submission.authorName)}</AvatarFallback>
|
||||
{submission.authorGithubUsername && (
|
||||
<AvatarImage src={githubAvatarUrl(submission.authorGithubUsername)} alt="" />
|
||||
)}
|
||||
<AvatarFallback>{avatarFallback(submission.authorGithubUsername)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="min-w-0">
|
||||
<CardTitle className="truncate text-base">{submission.title}</CardTitle>
|
||||
<a
|
||||
href={submission.authorGithubProfileUrl || undefined}
|
||||
target={submission.authorGithubProfileUrl ? "_blank" : undefined}
|
||||
href={githubProfileUrl(submission.authorGithubUsername) || undefined}
|
||||
target={submission.authorGithubUsername ? "_blank" : undefined}
|
||||
rel="noreferrer"
|
||||
className="text-xs text-muted-foreground hover:text-foreground hover:underline"
|
||||
>
|
||||
{submission.authorName}
|
||||
{submission.authorGithubUsername || "Community member"}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -237,7 +240,8 @@ export function WorkshopAdmin({ workshopUrl }: { workshopUrl?: string }) {
|
||||
<div className="min-w-0">
|
||||
<AlertTitle className="col-auto capitalize">{report.category} report</AlertTitle>
|
||||
<p className="m-0 mt-1 text-xs text-destructive/75">
|
||||
Reported by {report.reporterName} · {new Date(report.created).toLocaleString()}
|
||||
Reported by {report.reporterGithubUsername || "Community member"} ·{" "}
|
||||
{new Date(report.created).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
|
||||
import type { WorkshopSubmission } from "@site/src/lib/pocketbase";
|
||||
import type { SubmissionType } from "@site/src/lib/workshop-schema";
|
||||
import { githubAvatarUrl, githubProfileUrl } from "@homarr/workshop/schema";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
@@ -98,7 +99,8 @@ export const WorkshopApp = ({ workshopUrl }: { workshopUrl: string }) => {
|
||||
.filter((item) => includeOutdated || !item.outdated)
|
||||
.filter(
|
||||
(item) =>
|
||||
!q || [item.title, item.description, item.authorName].some((value) => value?.toLowerCase().includes(q)),
|
||||
!q ||
|
||||
[item.title, item.description, item.authorGithubUsername].some((value) => value?.toLowerCase().includes(q)),
|
||||
)
|
||||
.toSorted(workshop.sorters[sort])
|
||||
.map((s) => s.id);
|
||||
@@ -356,16 +358,18 @@ const SubmissionCard = ({ submission, backend, userVote, onVote }: SubmissionCar
|
||||
</div>
|
||||
<CardDescription className="flex min-w-0 items-center gap-1 text-xs">
|
||||
<a
|
||||
href={submission.authorGithubProfileUrl || undefined}
|
||||
target={submission.authorGithubProfileUrl ? "_blank" : undefined}
|
||||
href={githubProfileUrl(submission.authorGithubUsername) || undefined}
|
||||
target={submission.authorGithubUsername ? "_blank" : undefined}
|
||||
rel="noreferrer"
|
||||
className="inline-flex min-w-0 items-center gap-1.5 hover:text-foreground"
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
{submission.authorAvatarUrl && <AvatarImage src={submission.authorAvatarUrl} alt="" />}
|
||||
<AvatarFallback className="text-[9px]">{avatarFallback(submission.authorName)}</AvatarFallback>
|
||||
{submission.authorGithubUsername && (
|
||||
<AvatarImage src={githubAvatarUrl(submission.authorGithubUsername)} alt="" />
|
||||
)}
|
||||
<AvatarFallback className="text-[9px]">{avatarFallback(submission.authorGithubUsername)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="truncate">{submission.authorName}</span>
|
||||
<span className="truncate">{submission.authorGithubUsername || "Community member"}</span>
|
||||
</a>
|
||||
<span className="shrink-0 whitespace-nowrap">
|
||||
· v{submission.revision} · {formatRelativeTime(submission.created)}
|
||||
|
||||
Reference in new issue
Block a user