mirror of
https://github.com/papra-hq/papra.git
synced 2025-12-31 16:30:57 -06:00
Compare commits
1 Commits
2fa
...
loading-st
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05a8cadbf5 |
@@ -106,7 +106,7 @@ export const EmailLoginForm: Component = () => {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Button type="submit" class="w-full">{t('auth.login.form.submit')}</Button>
|
||||
<Button type="submit" class="w-full" isLoading={form.submitting}>{t('auth.login.form.submit')}</Button>
|
||||
|
||||
<div class="text-red-500 text-sm mt-4">{form.response.message}</div>
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export const EmailRegisterForm: Component = () => {
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Button type="submit" class="w-full">{t('auth.register.form.submit')}</Button>
|
||||
<Button type="submit" class="w-full" isLoading={form.submitting}>{t('auth.register.form.submit')}</Button>
|
||||
|
||||
<div class="text-red-500 text-sm mt-4">{form.response.message}</div>
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ResetPasswordForm: Component<{ onSubmit: (args: { newPassword: stri
|
||||
)}
|
||||
</Field>
|
||||
|
||||
<Button type="submit" class="w-full">
|
||||
<Button type="submit" class="w-full" isLoading={form.submitting}>
|
||||
{t('auth.reset-password.form.submit')}
|
||||
</Button>
|
||||
|
||||
|
||||
@@ -2,13 +2,14 @@ import type { DropdownMenuSubTriggerProps } from '@kobalte/core/dropdown-menu';
|
||||
import type { Component } from 'solid-js';
|
||||
import type { Document } from '../documents.types';
|
||||
import { A } from '@solidjs/router';
|
||||
import { Show } from 'solid-js';
|
||||
import { Button } from '@/modules/ui/components/button';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/modules/ui/components/dropdown-menu';
|
||||
import { useDeleteDocument } from '../documents.composables';
|
||||
import { useRenameDocumentDialog } from './rename-document-button.component';
|
||||
|
||||
export const DocumentManagementDropdown: Component<{ document: Document }> = (props) => {
|
||||
const { deleteDocument } = useDeleteDocument();
|
||||
const { deleteDocument, getIsDeletingDocument } = useDeleteDocument();
|
||||
const { openRenameDialog } = useRenameDocumentDialog();
|
||||
|
||||
const deleteDoc = () => deleteDocument({
|
||||
@@ -52,8 +53,14 @@ export const DocumentManagementDropdown: Component<{ document: Document }> = (pr
|
||||
<DropdownMenuItem
|
||||
class="cursor-pointer text-red"
|
||||
onClick={() => deleteDoc()}
|
||||
disabled={getIsDeletingDocument()}
|
||||
>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
<Show when={getIsDeletingDocument()}>
|
||||
<div class="i-tabler-loader-2 animate-spin size-4 mr-2" />
|
||||
</Show>
|
||||
<Show when={!getIsDeletingDocument()}>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
</Show>
|
||||
<span>Delete document</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -83,7 +83,7 @@ export const RenameDocumentDialog: Component<{
|
||||
<Button type="button" variant="secondary" onClick={() => props.setIsOpen(false)}>
|
||||
{t('documents.rename.cancel')}
|
||||
</Button>
|
||||
<Button type="submit">{t('documents.rename.form.submit')}</Button>
|
||||
<Button type="submit" isLoading={renameDocumentMutation.isPending}>{t('documents.rename.form.submit')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
|
||||
@@ -24,8 +24,10 @@ function getConfirmMessage(documentName: string) {
|
||||
|
||||
export function useDeleteDocument() {
|
||||
const { confirm } = useConfirmModal();
|
||||
const [getIsDeletingDocument, setIsDeletingDocument] = createSignal(false);
|
||||
|
||||
return {
|
||||
getIsDeletingDocument,
|
||||
async deleteDocument({ documentId, organizationId, documentName }: { documentId: string; organizationId: string; documentName: string }): Promise<{ hasDeleted: boolean }> {
|
||||
const isConfirmed = await confirm({
|
||||
title: 'Delete document',
|
||||
@@ -43,6 +45,8 @@ export function useDeleteDocument() {
|
||||
return { hasDeleted: false };
|
||||
}
|
||||
|
||||
setIsDeletingDocument(true);
|
||||
|
||||
await deleteDocument({
|
||||
documentId,
|
||||
organizationId,
|
||||
@@ -51,6 +55,8 @@ export function useDeleteDocument() {
|
||||
await invalidateOrganizationDocumentsQuery({ organizationId });
|
||||
createToast({ type: 'success', message: 'Document deleted' });
|
||||
|
||||
setIsDeletingDocument(false);
|
||||
|
||||
return { hasDeleted: true };
|
||||
},
|
||||
};
|
||||
|
||||
@@ -30,6 +30,7 @@ const AllowedOriginsDialog: Component<{
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
}> = (props) => {
|
||||
const [getAllowedOrigins, setAllowedOrigins] = createSignal(props.intakeEmails?.allowedOrigins || []);
|
||||
const [deletingOrigin, setDeletingOrigin] = createSignal<string | null>(null);
|
||||
const { t } = useI18n();
|
||||
|
||||
const update = async () => {
|
||||
@@ -45,8 +46,10 @@ const AllowedOriginsDialog: Component<{
|
||||
};
|
||||
|
||||
const deleteAllowedOrigin = async ({ origin }: { origin: string }) => {
|
||||
setDeletingOrigin(origin);
|
||||
setAllowedOrigins(origins => origins.filter(o => o !== origin));
|
||||
await update();
|
||||
setDeletingOrigin(null);
|
||||
};
|
||||
|
||||
const { form, Form, Field } = createForm({
|
||||
@@ -109,7 +112,7 @@ const AllowedOriginsDialog: Component<{
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<TextField type="email" id="email" placeholder={t('intake-emails.allowed-origins.add.placeholder')} {...inputProps} autoFocus value={field.value} aria-invalid={Boolean(field.error)} />
|
||||
<Button type="submit">
|
||||
<Button type="submit" isLoading={form.submitting}>
|
||||
<div class="i-tabler-plus size-4 mr-2" />
|
||||
{t('intake-emails.allowed-origins.add.button')}
|
||||
</Button>
|
||||
@@ -140,6 +143,7 @@ const AllowedOriginsDialog: Component<{
|
||||
size="icon"
|
||||
class="text-red"
|
||||
onClick={() => deleteAllowedOrigin({ origin })}
|
||||
isLoading={deletingOrigin() === origin}
|
||||
>
|
||||
<div class="i-tabler-trash size-4" />
|
||||
</Button>
|
||||
@@ -157,6 +161,9 @@ export const IntakeEmailsPage: Component = () => {
|
||||
const { t, te } = useI18n();
|
||||
const [selectedIntakeEmail, setSelectedIntakeEmail] = createSignal<IntakeEmail | null>(null);
|
||||
const [openDropdownId, setOpenDropdownId] = createSignal<string | null>(null);
|
||||
const [isCreatingEmail, setIsCreatingEmail] = createSignal(false);
|
||||
const [updatingEmailId, setUpdatingEmailId] = createSignal<string | null>(null);
|
||||
const [deletingEmailId, setDeletingEmailId] = createSignal<string | null>(null);
|
||||
|
||||
if (!config.intakeEmails.isEnabled) {
|
||||
return (
|
||||
@@ -195,6 +202,8 @@ export const IntakeEmailsPage: Component = () => {
|
||||
}));
|
||||
|
||||
const createEmail = async () => {
|
||||
setIsCreatingEmail(true);
|
||||
|
||||
const [,error] = await safely(createIntakeEmail({ organizationId: params.organizationId }));
|
||||
|
||||
if (error) {
|
||||
@@ -203,6 +212,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
setIsCreatingEmail(false);
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -212,6 +222,8 @@ export const IntakeEmailsPage: Component = () => {
|
||||
message: t('intake-emails.create.success'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
setIsCreatingEmail(false);
|
||||
};
|
||||
|
||||
const deleteEmail = async ({ intakeEmailId }: { intakeEmailId: string }) => {
|
||||
@@ -231,6 +243,8 @@ export const IntakeEmailsPage: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingEmailId(intakeEmailId);
|
||||
|
||||
await deleteIntakeEmail({ organizationId: params.organizationId, intakeEmailId });
|
||||
await query.refetch();
|
||||
|
||||
@@ -238,9 +252,13 @@ export const IntakeEmailsPage: Component = () => {
|
||||
message: t('intake-emails.delete.success'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
setDeletingEmailId(null);
|
||||
};
|
||||
|
||||
const updateEmail = async ({ intakeEmailId, isEnabled }: { intakeEmailId: string; isEnabled: boolean }) => {
|
||||
setUpdatingEmailId(intakeEmailId);
|
||||
|
||||
await updateIntakeEmail({ organizationId: params.organizationId, intakeEmailId, isEnabled });
|
||||
await query.refetch();
|
||||
|
||||
@@ -248,6 +266,8 @@ export const IntakeEmailsPage: Component = () => {
|
||||
message: isEnabled ? t('intake-emails.update.success.enabled') : t('intake-emails.update.success.disabled'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
setUpdatingEmailId(null);
|
||||
};
|
||||
|
||||
const openAllowedOriginsDialog = (intakeEmail: IntakeEmail) => {
|
||||
@@ -284,7 +304,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
class="pt-0"
|
||||
icon="i-tabler-mail"
|
||||
cta={(
|
||||
<Button variant="secondary" onClick={createEmail}>
|
||||
<Button variant="secondary" onClick={createEmail} isLoading={isCreatingEmail()}>
|
||||
<div class="i-tabler-plus size-4 mr-2" />
|
||||
{t('intake-emails.empty.generate')}
|
||||
</Button>
|
||||
@@ -301,7 +321,7 @@ export const IntakeEmailsPage: Component = () => {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Button onClick={createEmail}>
|
||||
<Button onClick={createEmail} isLoading={isCreatingEmail()}>
|
||||
<div class="i-tabler-plus size-4 mr-2" />
|
||||
{t('intake-emails.new')}
|
||||
</Button>
|
||||
@@ -359,8 +379,14 @@ export const IntakeEmailsPage: Component = () => {
|
||||
setOpenDropdownId(null);
|
||||
updateEmail({ intakeEmailId: intakeEmail.id, isEnabled: !intakeEmail.isEnabled });
|
||||
}}
|
||||
disabled={updatingEmailId() === intakeEmail.id}
|
||||
>
|
||||
<div class="i-tabler-power size-4 mr-2" />
|
||||
<Show when={updatingEmailId() === intakeEmail.id}>
|
||||
<div class="i-tabler-loader-2 animate-spin size-4 mr-2" />
|
||||
</Show>
|
||||
<Show when={updatingEmailId() !== intakeEmail.id}>
|
||||
<div class="i-tabler-power size-4 mr-2" />
|
||||
</Show>
|
||||
{intakeEmail.isEnabled ? t('intake-emails.actions.disable') : t('intake-emails.actions.enable')}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -377,8 +403,14 @@ export const IntakeEmailsPage: Component = () => {
|
||||
deleteEmail({ intakeEmailId: intakeEmail.id });
|
||||
}}
|
||||
class="text-red"
|
||||
disabled={deletingEmailId() === intakeEmail.id}
|
||||
>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
<Show when={deletingEmailId() === intakeEmail.id}>
|
||||
<div class="i-tabler-loader-2 animate-spin size-4 mr-2" />
|
||||
</Show>
|
||||
<Show when={deletingEmailId() !== intakeEmail.id}>
|
||||
<div class="i-tabler-trash size-4 mr-2" />
|
||||
</Show>
|
||||
{t('intake-emails.actions.delete')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { OrganizationMemberRole } from '../organizations.types';
|
||||
import { A, useParams } from '@solidjs/router';
|
||||
import { useMutation, useQuery } from '@tanstack/solid-query';
|
||||
import { createSolidTable, flexRender, getCoreRowModel, getPaginationRowModel } from '@tanstack/solid-table';
|
||||
import { For, Show } from 'solid-js';
|
||||
import { createSignal, For, Show } from 'solid-js';
|
||||
import { useI18n } from '@/modules/i18n/i18n.provider';
|
||||
import { useConfirmModal } from '@/modules/shared/confirm';
|
||||
import { useI18nApiErrors } from '@/modules/shared/http/composables/i18n-api-errors';
|
||||
@@ -30,6 +30,9 @@ const MemberList: Component = () => {
|
||||
|
||||
const { getIsAtLeastAdmin, getRole } = useCurrentUserRole({ organizationId: params.organizationId });
|
||||
|
||||
const [deletingMemberId, setDeletingMemberId] = createSignal<string | null>(null);
|
||||
const [updatingMemberId, setUpdatingMemberId] = createSignal<string | null>(null);
|
||||
|
||||
const removeMemberMutation = useMutation(() => ({
|
||||
mutationFn: ({ memberId }: { memberId: string }) => removeOrganizationMember({ organizationId: params.organizationId, memberId }),
|
||||
onSuccess: () => {
|
||||
@@ -75,11 +78,23 @@ const MemberList: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
removeMemberMutation.mutate({ memberId });
|
||||
setDeletingMemberId(memberId);
|
||||
try {
|
||||
await removeMemberMutation.mutateAsync({ memberId });
|
||||
}
|
||||
finally {
|
||||
setDeletingMemberId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateMemberRole = async ({ memberId, role }: { memberId: string; role: OrganizationMemberRole }) => {
|
||||
await updateMemberRoleMutation.mutateAsync({ memberId, role });
|
||||
setUpdatingMemberId(memberId);
|
||||
try {
|
||||
await updateMemberRoleMutation.mutateAsync({ memberId, role });
|
||||
}
|
||||
finally {
|
||||
setUpdatingMemberId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const table = createSolidTable({
|
||||
@@ -99,9 +114,14 @@ const MemberList: Component = () => {
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete({ memberId: data.row.original.id })}
|
||||
disabled={data.row.original.role === ORGANIZATION_ROLES.OWNER || !getIsAtLeastAdmin()}
|
||||
disabled={data.row.original.role === ORGANIZATION_ROLES.OWNER || !getIsAtLeastAdmin() || deletingMemberId() === data.row.original.id}
|
||||
>
|
||||
<div class="i-tabler-user-x size-4 mr-2" />
|
||||
<Show when={deletingMemberId() === data.row.original.id}>
|
||||
<div class="i-tabler-loader-2 animate-spin size-4 mr-2" />
|
||||
</Show>
|
||||
<Show when={deletingMemberId() !== data.row.original.id}>
|
||||
<div class="i-tabler-user-x size-4 mr-2" />
|
||||
</Show>
|
||||
{t('organizations.members.remove-from-organization')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -111,19 +131,19 @@ const MemberList: Component = () => {
|
||||
<DropdownMenuRadioGroup value={data.row.original.role} onChange={role => handleUpdateMemberRole({ memberId: data.row.original.id, role: role as OrganizationMemberRole })}>
|
||||
<DropdownMenuRadioItem
|
||||
value={ORGANIZATION_ROLES.OWNER}
|
||||
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.OWNER })}
|
||||
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.OWNER }) || updatingMemberId() === data.row.original.id}
|
||||
>
|
||||
{t(`organizations.members.roles.owner`)}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem
|
||||
value={ORGANIZATION_ROLES.ADMIN}
|
||||
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.ADMIN })}
|
||||
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.ADMIN }) || updatingMemberId() === data.row.original.id}
|
||||
>
|
||||
{t(`organizations.members.roles.admin`)}
|
||||
</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem
|
||||
value={ORGANIZATION_ROLES.MEMBER}
|
||||
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.MEMBER })}
|
||||
disabled={getIsMemberRoleDisabled({ currentUserRole: getRole(), memberRole: data.row.original.role, targetRole: ORGANIZATION_ROLES.MEMBER }) || updatingMemberId() === data.row.original.id}
|
||||
>
|
||||
{t(`organizations.members.roles.member`)}
|
||||
</DropdownMenuRadioItem>
|
||||
|
||||
@@ -284,7 +284,7 @@ export const TaggingRuleForm: Component<{
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Button type="submit">{props.submitButtonText ?? t('tagging-rules.form.submit')}</Button>
|
||||
<Button type="submit" isLoading={form.submitting}>{props.submitButtonText ?? t('tagging-rules.form.submit')}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ const TagForm: Component<{
|
||||
</Field>
|
||||
|
||||
<div class="flex flex-row-reverse justify-between items-center mt-6">
|
||||
<Button type="submit">
|
||||
<Button type="submit" isLoading={form.submitting}>
|
||||
{props.submitLabel ?? t('tags.create')}
|
||||
</Button>
|
||||
|
||||
@@ -229,6 +229,7 @@ export const TagsPage: Component = () => {
|
||||
const { confirm } = useConfirmModal();
|
||||
const { t } = useI18n();
|
||||
const { getErrorMessage } = useI18nApiErrors({ t });
|
||||
const [deletingTagId, setDeletingTagId] = createSignal<string | null>(null);
|
||||
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ['organizations', params.organizationId, 'tags'],
|
||||
@@ -253,6 +254,8 @@ export const TagsPage: Component = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingTagId(tag.id);
|
||||
|
||||
const [, error] = await safely(deleteTag({
|
||||
organizationId: params.organizationId,
|
||||
tagId: tag.id,
|
||||
@@ -264,6 +267,7 @@ export const TagsPage: Component = () => {
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
setDeletingTagId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -276,6 +280,8 @@ export const TagsPage: Component = () => {
|
||||
message: t('tags.delete.success'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
setDeletingTagId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -368,7 +374,7 @@ export const TagsPage: Component = () => {
|
||||
)}
|
||||
</UpdateTagModal>
|
||||
|
||||
<Button size="icon" variant="outline" class="size-7 text-red" onClick={() => del({ tag })}>
|
||||
<Button size="icon" variant="outline" class="size-7 text-red" onClick={() => del({ tag })} isLoading={deletingTagId() === tag.id}>
|
||||
<div class="i-tabler-trash size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { For } from 'solid-js';
|
||||
import { Skeleton } from '../skeleton';
|
||||
|
||||
type cardSkeletonProps = {
|
||||
lines?: number;
|
||||
};
|
||||
|
||||
export const CardSkeleton: Component<cardSkeletonProps> = (props) => {
|
||||
const lines = () => props.lines ?? 3;
|
||||
|
||||
return (
|
||||
<div class="border border-border rounded-lg p-4">
|
||||
<Skeleton class="h-6 w-1/3 mb-3" />
|
||||
<div class="space-y-2">
|
||||
<For each={Array.from({ length: lines() })}>
|
||||
{(_, index) => (
|
||||
<Skeleton class={`h-4 ${index() === lines() - 1 ? 'w-2/3' : 'w-full'}`} />
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { For } from 'solid-js';
|
||||
import { CardSkeleton } from './card-skeleton';
|
||||
|
||||
type gridSkeletonProps = {
|
||||
items?: number;
|
||||
columns?: number;
|
||||
};
|
||||
|
||||
export const GridSkeleton: Component<gridSkeletonProps> = (props) => {
|
||||
const items = () => props.items ?? 6;
|
||||
const columns = () => props.columns ?? 3;
|
||||
|
||||
return (
|
||||
<div
|
||||
class="grid gap-4"
|
||||
style={{
|
||||
'grid-template-columns': `repeat(${columns()}, minmax(0, 1fr))`,
|
||||
}}
|
||||
>
|
||||
<For each={Array.from({ length: items() })}>
|
||||
{() => <CardSkeleton />}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export { TableSkeleton } from './table-skeleton';
|
||||
export { CardSkeleton } from './card-skeleton';
|
||||
export { GridSkeleton } from './grid-skeleton';
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Component } from 'solid-js';
|
||||
import { For } from 'solid-js';
|
||||
import { Skeleton } from '../skeleton';
|
||||
|
||||
type tableSkeletonProps = {
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
};
|
||||
|
||||
export const TableSkeleton: Component<tableSkeletonProps> = (props) => {
|
||||
const rows = () => props.rows ?? 5;
|
||||
const columns = () => props.columns ?? 4;
|
||||
|
||||
return (
|
||||
<div class="w-full">
|
||||
<For each={Array.from({ length: rows() })}>
|
||||
{() => (
|
||||
<div class="flex gap-4 py-3 border-b border-border/80">
|
||||
<For each={Array.from({ length: columns() })}>
|
||||
{(_, index) => (
|
||||
<div class={index() === 0 ? 'flex-1' : 'w-24'}>
|
||||
<Skeleton class="h-5 w-full" />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user