diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx
index 46074422104..b06c5b5de14 100644
--- a/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx
+++ b/specifyweb/frontend/js_src/lib/components/AppResources/EditorWrapper.tsx
@@ -8,6 +8,7 @@ import { fetchResource } from '../DataModel/resource';
import type { SpAppResource, SpViewSetObj } from '../DataModel/types';
import { NotFoundView } from '../Router/NotFoundView';
import { locationToState, useStableLocation } from '../Router/RouterState';
+import { AppResourceSkeleton } from '../SkeletonLoaders/AppResource';
import { findAppResourceDirectory } from './Create';
import { AppResourceEditor } from './Editor';
import type { AppResourceMode } from './helpers';
@@ -15,7 +16,7 @@ import { getAppResourceMode } from './helpers';
import type { AppResources } from './hooks';
import { useResourcesTree } from './hooks';
import type { AppResourcesOutlet } from './index';
-import { ScopedAppResourceDir } from './types';
+import type { ScopedAppResourceDir } from './types';
export function AppResourceView(): JSX.Element {
return ;
@@ -43,8 +44,9 @@ export function Wrapper({
const baseHref = `/specify/resources/${
mode === 'appResources' ? 'app-resource' : 'view-set'
}`;
- return initialData === undefined ? null : resource === undefined ||
- directory === undefined ? (
+ return initialData === undefined ? (
+
+ ) : resource === undefined || directory === undefined ? (
) : (
))}
- {isComplete
- ? attachments.length === 0 &&
{attachmentsText.noAttachments()}
- : loadingGif}
+ {isComplete ? (
+ attachments.length === 0 && {attachmentsText.noAttachments()}
+ ) : (
+
+ )}
{typeof viewRecord === 'object' && (
diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx b/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx
index cbd6b70847e..81ae091c9af 100644
--- a/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx
@@ -24,6 +24,7 @@ import { loadingBar } from '../Molecules';
import { Dialog } from '../Molecules/Dialog';
import { FilePicker } from '../Molecules/FilePicker';
import { ProtectedTable } from '../Permissions/PermissionDenied';
+import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin';
import { attachmentSettingsPromise, uploadFile } from './attachments';
import { AttachmentViewer } from './Viewer';
@@ -52,7 +53,7 @@ export function useAttachment(
false,
[resource]
),
- true
+ false
);
}
@@ -64,13 +65,16 @@ function ProtectedAttachmentsPlugin({
readonly mode: FormMode;
}): JSX.Element | null {
const [attachment, setAttachment] = useAttachment(resource);
+
useErrorContext('attachment', attachment);
const filePickerContainer = React.useRef(null);
const related = useTriggerState(
resource?.specifyModel.name === 'Attachment' ? undefined : resource
);
- return attachment === undefined ? null : (
+ return attachment === undefined ? (
+
+ ) : (
{formsText.noData()}
) : (
- loadingGif
+
);
}
diff --git a/specifyweb/frontend/js_src/lib/components/Forms/SpecifyForm.tsx b/specifyweb/frontend/js_src/lib/components/Forms/SpecifyForm.tsx
index e8b2d021360..c313c7dd136 100644
--- a/specifyweb/frontend/js_src/lib/components/Forms/SpecifyForm.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Forms/SpecifyForm.tsx
@@ -19,6 +19,7 @@ import { attachmentView } from '../FormParse/webOnlyViews';
import { loadingGif } from '../Molecules';
import { userPreferences } from '../Preferences/userPreferences';
import { unsafeTriggerNotFound } from '../Router/Router';
+import { FormSkeleton } from '../SkeletonLoaders/Form';
const FormLoadingContext = React.createContext
(false);
FormLoadingContext.displayName = 'FormLoadingContext';
@@ -74,8 +75,7 @@ export function SpecifyForm({
// If parent resource is loading, don't duplicate the loading bar in children
const isAlreadyLoading = React.useContext(FormLoadingContext);
- const showLoading =
- !isAlreadyLoading && (!formIsLoaded || isLoading || isShowingOldResource);
+ const showLoading = !isAlreadyLoading && (isLoading || isShowingOldResource);
const [flexibleColumnWidth] = userPreferences.use(
'form',
'definition',
@@ -97,21 +97,21 @@ export function SpecifyForm({
{showLoading && (
{loadingGif}
)}
- {formIsLoaded && (
+ {formIsLoaded ? (
({
))}
+ ) : (
+
)}
diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx
index d1a57ee2060..85aa323e4cb 100644
--- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx
+++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Wrapped.tsx
@@ -25,6 +25,7 @@ import { isTreeModel, treeRanksPromise } from '../InitialContext/treeRanks';
import { useTitle } from '../Molecules/AppTitle';
import { hasPermission, hasToolPermission } from '../Permissions/helpers';
import { userPreferences } from '../Preferences/userPreferences';
+import { QueryBuilderSkeleton } from '../SkeletonLoaders/QueryBuilder';
import { getMappedFields, mappingPathIsComplete } from '../WbPlanView/helpers';
import { getMappingLineProps } from '../WbPlanView/LineComponents';
import { MappingView } from '../WbPlanView/MapperComponents';
@@ -92,7 +93,7 @@ export function QueryBuilder({
'queryBuilder',
queryResource.isNew() ? 'create' : 'update'
);
- const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, true);
+ const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, false);
const [query, setQuery] = useResource(queryResource);
useErrorContext('query', query);
@@ -557,5 +558,7 @@ export function QueryBuilder({
)}
- ) : null;
+ ) : (
+
+ );
}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AppResource.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AppResource.tsx
new file mode 100644
index 00000000000..bcf2902b2c5
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AppResource.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import { Skeleton } from './Skeleton';
+
+export function AppResourceSkeleton() {
+ return (
+
+
+
+ {Array.from({ length: 15 }, (_, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AttachmentGallery.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AttachmentGallery.tsx
new file mode 100644
index 00000000000..534f12b2bba
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AttachmentGallery.tsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { DEFAULT_FETCH_LIMIT } from '../DataModel/collection';
+import { Skeleton } from './Skeleton';
+
+export function AttachmentGallerySkeleton() {
+ return (
+
+ {Array.from({ length: DEFAULT_FETCH_LIMIT }, (_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AttachmentPlugin.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AttachmentPlugin.tsx
new file mode 100644
index 00000000000..a68439bb6ac
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/AttachmentPlugin.tsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import { Skeleton } from './Skeleton';
+
+export function AttachmentPluginSkeleton() {
+ return (
+
+
+
+
+
+
+
+ {Array.from({ length: 6 }, (_, index) => (
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/DialogList.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/DialogList.tsx
new file mode 100644
index 00000000000..42f292caf47
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/DialogList.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+
+import { Skeleton } from './Skeleton';
+
+export function DialogListSkeleton() {
+ return (
+
+
+
+
+ {Array.from({ length: 5 }, (_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/Form.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/Form.tsx
new file mode 100644
index 00000000000..b59feb082e1
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/Form.tsx
@@ -0,0 +1,95 @@
+import React from 'react';
+
+import { Skeleton } from './Skeleton';
+
+export function FormSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/QueryBuilder.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/QueryBuilder.tsx
new file mode 100644
index 00000000000..fe9cbf7902c
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/QueryBuilder.tsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import { Skeleton } from './Skeleton';
+
+export function QueryBuilderSkeleton() {
+ return (
+
+
+
+
+ );
+}
diff --git a/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/Skeleton.tsx b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/Skeleton.tsx
new file mode 100644
index 00000000000..8139f69a08d
--- /dev/null
+++ b/specifyweb/frontend/js_src/lib/components/SkeletonLoaders/Skeleton.tsx
@@ -0,0 +1,39 @@
+import { commonText } from '../../localization/common';
+import { wrap } from '../Atoms/wrapper';
+
+const skeleton = `bg-gray-200 dark:bg-neutral-600 [body:not(.reduce-motion)_&]:animate-pulse`;
+
+export const Skeleton = {
+ Root: wrap('Skeleton.Root', 'div', 'flex gap-4', {
+ 'aria-label': commonText.loading(),
+ }),
+ Line: wrap('Skeletons.Line', 'div', `${skeleton} h-4 w-14 rounded`),
+ LongLine: wrap('Skeletons.LongLine', 'div', `${skeleton} h-4 rounded`),
+ Square: wrap('Skeletons.Square', 'div', `${skeleton} h-32 rounded`),
+ SmallSquare: wrap(
+ 'Skeletons.SmallSquare',
+ 'div',
+ `${skeleton} h-6 w-6 rounded`
+ ),
+ Rectangle: wrap('Skeletons.Rectangle', 'div', `${skeleton} h-6 w-64 rounded`),
+ SmallRectangle: wrap(
+ 'Skeletons.SmallRectangle',
+ 'div',
+ `${skeleton} h-6 w-16 rounded`
+ ),
+ TallRectangle: wrap(
+ 'Skeletons.TallRectangle',
+ 'div',
+ `${skeleton} w-32 rounded`
+ ),
+ ThinRectangle: wrap(
+ 'Skeletons.ThinRectangle',
+ 'div',
+ `${skeleton} w-32 w-6 rounded`
+ ),
+ SmallCircle: wrap(
+ 'Skeletons.SmallCircle',
+ 'div',
+ `${skeleton} w-6 rounded-full`
+ ),
+};
diff --git a/specifyweb/frontend/js_src/lib/components/Toolbar/Query.tsx b/specifyweb/frontend/js_src/lib/components/Toolbar/Query.tsx
index b80380b35ac..e155298c604 100644
--- a/specifyweb/frontend/js_src/lib/components/Toolbar/Query.tsx
+++ b/specifyweb/frontend/js_src/lib/components/Toolbar/Query.tsx
@@ -28,6 +28,7 @@ import { hasPermission, hasToolPermission } from '../Permissions/helpers';
import { QueryEditButton } from '../QueryBuilder/Edit';
import { OverlayContext } from '../Router/Router';
import { SafeOutlet } from '../Router/RouterUtils';
+import { DialogListSkeleton } from '../SkeletonLoaders/DialogList';
import { QueryTablesWrapper } from './QueryTablesWrapper';
export function QueriesOverlay(): JSX.Element {
@@ -66,7 +67,7 @@ export function useQueries(
}).then(({ records }) => records),
[spQueryFilter]
),
- true
+ false
);
React.useEffect(
() =>
@@ -91,7 +92,16 @@ export function QueryListDialog({
getQuerySelectUrl,
isReadOnly,
}: QueryListContextType): JSX.Element | null {
- return Array.isArray(queries) ? (
+ return queries === undefined ? (
+
+ ) : Array.isArray(queries) ? (