feat(web): standardize loading on shared skeleton recipes; retire '…' + empty status divs (#53)
This commit is contained in:
+4
-7
@@ -12,6 +12,7 @@ import { VocabulariesPage } from "./vocab/vocabularies-page";
|
||||
import { VocabularyTerms } from "./vocab/vocabulary-terms";
|
||||
import { SelectVocabularyPrompt } from "./vocab/select-vocabulary-prompt";
|
||||
import { AuthoritiesPage } from "./authorities/authorities-page";
|
||||
import { FormSkeleton, ListSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
const ObjectNewPage = lazy(() =>
|
||||
import("./objects/object-new-page").then((m) => ({ default: m.ObjectNewPage })),
|
||||
@@ -25,10 +26,6 @@ const FieldsPage = lazy(() =>
|
||||
import("./fields/fields-page").then((m) => ({ default: m.FieldsPage })),
|
||||
);
|
||||
|
||||
function FormFallback() {
|
||||
return <div role="status" className="p-4 text-sm text-muted-foreground">Loading…</div>;
|
||||
}
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<>
|
||||
@@ -38,7 +35,7 @@ const router = createBrowserRouter(
|
||||
<Route
|
||||
path="/objects/new"
|
||||
element={
|
||||
<Suspense fallback={<FormFallback />}>
|
||||
<Suspense fallback={<div className="mx-auto max-w-2xl"><FormSkeleton /></div>}>
|
||||
<ObjectNewPage />
|
||||
</Suspense>
|
||||
}
|
||||
@@ -48,7 +45,7 @@ const router = createBrowserRouter(
|
||||
<Route
|
||||
path=":id/edit"
|
||||
element={
|
||||
<Suspense fallback={<FormFallback />}>
|
||||
<Suspense fallback={<div className="mx-auto max-w-2xl"><FormSkeleton /></div>}>
|
||||
<ObjectEditForm />
|
||||
</Suspense>
|
||||
}
|
||||
@@ -67,7 +64,7 @@ const router = createBrowserRouter(
|
||||
<Route
|
||||
path="/fields"
|
||||
element={
|
||||
<Suspense fallback={<FormFallback />}>
|
||||
<Suspense fallback={<ListSkeleton />}>
|
||||
<FieldsPage />
|
||||
</Suspense>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
|
||||
import { useMe } from "../api/queries";
|
||||
import { AppShellSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
export function RequireAuth() {
|
||||
const { data: user, isLoading } = useMe();
|
||||
|
||||
if (isLoading) return <div role="status" aria-label="loading" />;
|
||||
if (isLoading) return <AppShellSkeleton />;
|
||||
|
||||
if (!user) return <Navigate to="/login" replace />;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useAuthorities, useCreateAuthority } from "../api/queries";
|
||||
import { LabelEditor } from "../components/label-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
import { ListSkeleton } from "@/components/ui/skeletons";
|
||||
import { AuthorityRow } from "./authority-row";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
||||
@@ -68,20 +69,21 @@ export function AuthoritiesPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<ul className="mb-4">
|
||||
{isLoading && (
|
||||
<li className="text-sm text-muted-foreground">…</li>
|
||||
)}
|
||||
{isError && (
|
||||
<li className="text-sm text-destructive">{t("authorities.loadError")}</li>
|
||||
)}
|
||||
{!isLoading && !isError && authorities?.length === 0 && (
|
||||
<li className="text-sm text-muted-foreground">{t("authorities.empty")}</li>
|
||||
)}
|
||||
{authorities?.map((a) => (
|
||||
<AuthorityRow key={a.id} authority={a} kind={currentKind} lang={lang} />
|
||||
))}
|
||||
</ul>
|
||||
{isLoading ? (
|
||||
<ListSkeleton className="mb-4" rows={5} />
|
||||
) : (
|
||||
<ul className="mb-4">
|
||||
{isError && (
|
||||
<li className="text-sm text-destructive">{t("authorities.loadError")}</li>
|
||||
)}
|
||||
{!isError && authorities?.length === 0 && (
|
||||
<li className="text-sm text-muted-foreground">{t("authorities.empty")}</li>
|
||||
)}
|
||||
{authorities?.map((a) => (
|
||||
<AuthorityRow key={a.id} authority={a} kind={currentKind} lang={lang} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<form onSubmit={onCreate} className="space-y-2 border-t pt-3">
|
||||
<div className="text-sm font-medium">
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { components } from "../api/schema";
|
||||
import { useFieldDefinitions, useDeleteFieldDefinition } from "../api/queries";
|
||||
import { labelText } from "../lib/labels";
|
||||
import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ListSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
type FieldDefinitionView = components["schemas"]["FieldDefinitionView"];
|
||||
|
||||
@@ -20,15 +20,7 @@ export function FieldList({
|
||||
const deleteField = useDeleteFieldDefinition();
|
||||
const lang = i18n.language.startsWith("sv") ? "sv" : "en";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-2 p-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-9 w-full" />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isLoading) return <ListSkeleton rows={6} />;
|
||||
|
||||
if (isError) return <p className="p-4 text-sm text-destructive">{t("fields.loadError")}</p>;
|
||||
if (!data || data.length === 0)
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { components } from "../api/schema";
|
||||
import { useObject, useUpdateObject, useSetFields, FieldRejection } from "../api/queries";
|
||||
import { useBreadcrumb } from "../shell/use-breadcrumb";
|
||||
import { ObjectForm, type ObjectCore, type ObjectFormValues } from "./object-form";
|
||||
import { FormSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
type AdminObjectView = components["schemas"]["AdminObjectView"];
|
||||
|
||||
@@ -15,7 +16,7 @@ export function ObjectEditForm() {
|
||||
|
||||
const { data: object, isLoading } = useObject(id!);
|
||||
|
||||
if (isLoading) return <div className="p-4" role="status" aria-label="loading" />;
|
||||
if (isLoading) return <FormSkeleton />;
|
||||
|
||||
if (!object) return <p className="p-4 text-sm text-muted-foreground">{t("objects.notFound")}</p>;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useDebouncedValue } from "../lib/use-debounced-value";
|
||||
import { SearchResultRow } from "./search-result-row";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ListSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
const VIS = ["all", "draft", "internal", "public"] as const;
|
||||
|
||||
@@ -84,11 +84,7 @@ export function SearchPanel() {
|
||||
{!hasQuery && <p className="p-4 text-sm text-muted-foreground">{t("search.prompt")}</p>}
|
||||
|
||||
{hasQuery && search.isLoading && (
|
||||
<div className="space-y-2 p-3">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
))}
|
||||
</div>
|
||||
<ListSkeleton rows={5} rowClassName="h-12 w-full" />
|
||||
)}
|
||||
|
||||
{hasQuery && search.isError && (
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DeleteConfirmDialog } from "../components/delete-confirm-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ListSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
export function VocabularyList() {
|
||||
const { t } = useTranslation();
|
||||
@@ -50,17 +51,17 @@ export function VocabularyList() {
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
<ul className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
<li className="p-3 text-sm text-muted-foreground">…</li>
|
||||
)}
|
||||
{isError && (
|
||||
<li className="p-3 text-sm text-destructive">{t("vocab.loadError")}</li>
|
||||
)}
|
||||
{data?.length === 0 && (
|
||||
<li className="p-3 text-sm text-muted-foreground">{t("vocab.empty")}</li>
|
||||
)}
|
||||
{data?.map((v) => (
|
||||
{isLoading ? (
|
||||
<ListSkeleton className="flex-1 overflow-auto" />
|
||||
) : (
|
||||
<ul className="flex-1 overflow-auto">
|
||||
{isError && (
|
||||
<li className="p-3 text-sm text-destructive">{t("vocab.loadError")}</li>
|
||||
)}
|
||||
{data?.length === 0 && (
|
||||
<li className="p-3 text-sm text-muted-foreground">{t("vocab.empty")}</li>
|
||||
)}
|
||||
{data?.map((v) => (
|
||||
<li key={v.id} className="flex items-center gap-1 border-b pr-2">
|
||||
{editingId === v.id ? (
|
||||
<form
|
||||
@@ -115,8 +116,9 @@ export function VocabularyList() {
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TermRow } from "./term-row";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ListSkeleton } from "@/components/ui/skeletons";
|
||||
|
||||
type LabelInput = components["schemas"]["LabelInput"];
|
||||
|
||||
@@ -62,20 +63,21 @@ export function VocabularyTerms() {
|
||||
<div className="mb-2 label-caption">
|
||||
{t("vocab.terms")}
|
||||
</div>
|
||||
<ul className="mb-4">
|
||||
{isLoading && (
|
||||
<li className="text-sm text-muted-foreground">…</li>
|
||||
)}
|
||||
{isError && (
|
||||
<li className="text-sm text-destructive">{t("vocab.loadError")}</li>
|
||||
)}
|
||||
{!isLoading && !isError && terms?.length === 0 && (
|
||||
<li className="text-sm text-muted-foreground">{t("vocab.noTerms")}</li>
|
||||
)}
|
||||
{terms?.map((term) => (
|
||||
<TermRow key={term.id} vocabularyId={id} term={term} lang={lang} />
|
||||
))}
|
||||
</ul>
|
||||
{isLoading ? (
|
||||
<ListSkeleton className="mb-4" rows={5} />
|
||||
) : (
|
||||
<ul className="mb-4">
|
||||
{isError && (
|
||||
<li className="text-sm text-destructive">{t("vocab.loadError")}</li>
|
||||
)}
|
||||
{!isError && terms?.length === 0 && (
|
||||
<li className="text-sm text-muted-foreground">{t("vocab.noTerms")}</li>
|
||||
)}
|
||||
{terms?.map((term) => (
|
||||
<TermRow key={term.id} vocabularyId={id} term={term} lang={lang} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<form onSubmit={onAdd} className="space-y-2 border-t pt-3">
|
||||
<div className="text-sm font-medium">{t("vocab.addTerm")}</div>
|
||||
<LabelEditor value={labels} onChange={setLabels} />
|
||||
|
||||
Reference in New Issue
Block a user