Paginate admin panel app registrations list (#22734)
## Context The `findAllApplicationRegistrations` query on the admin panel Apps page (`/settings/admin-panel#apps`) loaded every application registration at once, with search and filtering done client-side. ## Changes **Server** - `findAllApplicationRegistrations` now takes `limit` / `offset` / `searchTerm` / `isPreInstalledOnly` args and returns a `PaginatedApplicationRegistrations` object (`registrations`, `totalCount`, `hasMore`), following the same pattern as `getQueueJobs`. - `ApplicationRegistrationService.findAll` uses `findAndCount` with `take`/`skip`, and moves the search (name, source package, universal identifier via `ILIKE`) and the pre-installed filter into the SQL query, mirroring how `getInstalledWorkspacesGlobal` filters installed workspaces. **Frontend** - `SettingsAdminApps` passes the page, the debounced search term (300ms, like the installed workspaces table), and the pre-installed toggle as query variables instead of filtering client-side. - Adds a Previous / Next pagination footer (25 per page) matching the queue jobs table, shown only when there is more than one page. - The "unconfigured first" ordering is kept within each page (`isConfigured` is a dataloader-resolved field, so it can't be sorted in SQL). ## Notes - Regenerated `generated-admin/graphql.ts` follows in a subsequent commit. --- _Generated by [Claude Code](https://claude.ai/code/session_015erumgPozkbNA3zPeKrrFW)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22734?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
+101
-46
@@ -1,5 +1,6 @@
|
||||
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import { StyledNameTableCell } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@@ -14,9 +15,19 @@ import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useContext, useState } from 'react';
|
||||
import { assertUnreachable, getSettingsPath } from 'twenty-shared/utils';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import {
|
||||
assertUnreachable,
|
||||
getSettingsPath,
|
||||
isDefined,
|
||||
} from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { IconChevronRight, IconPinned, IconRefresh } from 'twenty-ui/icon';
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconDotsVertical,
|
||||
IconPinned,
|
||||
IconRefresh,
|
||||
} from 'twenty-ui/icon';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import { Button, SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
@@ -35,18 +46,36 @@ const StyledTableContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledShowMoreContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const TABLE_GRID = '1fr 100px 100px 100px 40px';
|
||||
const TABLE_GRID_MOBILE = '3fr 3fr 1fr 1fr 40px';
|
||||
const PAGE_SIZE = 25;
|
||||
|
||||
export const SettingsAdminApps = () => {
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [debouncedSearchQuery] = useDebounce(searchQuery, 300);
|
||||
const [showPreInstalledOnly, setShowPreInstalledOnly] = useState(false);
|
||||
|
||||
const { data } = useQuery(FindAllApplicationRegistrationsDocument, {
|
||||
client: apolloAdminClient,
|
||||
});
|
||||
const { data, loading, fetchMore } = useQuery(
|
||||
FindAllApplicationRegistrationsDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
notifyOnNetworkStatusChange: true,
|
||||
variables: {
|
||||
limit: PAGE_SIZE,
|
||||
offset: 0,
|
||||
searchTerm: debouncedSearchQuery,
|
||||
isPreInstalledOnly: showPreInstalledOnly,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const [syncMarketplaceCatalog, { loading: isSyncing }] = useMutation(
|
||||
SyncMarketplaceCatalogDocument,
|
||||
@@ -66,27 +95,35 @@ export const SettingsAdminApps = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const registrations = data?.findAllApplicationRegistrations ?? [];
|
||||
const registrations = [
|
||||
...(data?.findAllApplicationRegistrations.registrations ?? []),
|
||||
].sort((a, b) => Number(a.isConfigured) - Number(b.isConfigured));
|
||||
|
||||
const query = searchQuery.trim().toLowerCase();
|
||||
const hasMore = data?.findAllApplicationRegistrations.hasMore ?? false;
|
||||
|
||||
const filtered = registrations
|
||||
.filter((registration) => {
|
||||
if (showPreInstalledOnly && !registration.isPreInstalled) {
|
||||
return false;
|
||||
}
|
||||
const handleShowMore = () => {
|
||||
fetchMore({
|
||||
variables: {
|
||||
limit: PAGE_SIZE,
|
||||
offset: registrations.length,
|
||||
},
|
||||
updateQuery: (previousData, { fetchMoreResult }) => {
|
||||
if (!isDefined(fetchMoreResult)) {
|
||||
return previousData;
|
||||
}
|
||||
|
||||
if (query.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
registration.name.toLowerCase().includes(query) ||
|
||||
(registration.sourcePackage ?? '').toLowerCase().includes(query) ||
|
||||
registration.universalIdentifier.toLowerCase().includes(query)
|
||||
);
|
||||
})
|
||||
.toSorted((a, b) => Number(a.isConfigured) - Number(b.isConfigured));
|
||||
return {
|
||||
findAllApplicationRegistrations: {
|
||||
...fetchMoreResult.findAllApplicationRegistrations,
|
||||
registrations: [
|
||||
...previousData.findAllApplicationRegistrations.registrations,
|
||||
...fetchMoreResult.findAllApplicationRegistrations.registrations,
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getFormattedSource = (
|
||||
registration: ApplicationRegistrationFragmentFragment,
|
||||
@@ -159,29 +196,47 @@ export const SettingsAdminApps = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<TableRow
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Source`}</TableHeader>
|
||||
<TableHeader align="right">{t`Listed`}</TableHeader>
|
||||
<TableHeader align="right">{t`Configured`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{filtered.map((registration) => (
|
||||
<SettingsAdminAppsTableRow
|
||||
key={registration.id}
|
||||
registration={registration}
|
||||
getFormattedSource={getFormattedSource}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
{loading && registrations.length === 0 ? (
|
||||
<SettingsEmptyPlaceholder>{t`Loading apps...`}</SettingsEmptyPlaceholder>
|
||||
) : registrations.length === 0 ? (
|
||||
<SettingsEmptyPlaceholder>{t`No apps found`}</SettingsEmptyPlaceholder>
|
||||
) : (
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<TableRow
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Source`}</TableHeader>
|
||||
<TableHeader align="right">{t`Listed`}</TableHeader>
|
||||
<TableHeader align="right">{t`Configured`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{registrations.map((registration) => (
|
||||
<SettingsAdminAppsTableRow
|
||||
key={registration.id}
|
||||
registration={registration}
|
||||
getFormattedSource={getFormattedSource}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
)}
|
||||
{hasMore && (
|
||||
<StyledShowMoreContainer>
|
||||
<Button
|
||||
title={t`Show more`}
|
||||
Icon={IconDotsVertical}
|
||||
onClick={handleShowMore}
|
||||
disabled={loading}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
/>
|
||||
</StyledShowMoreContainer>
|
||||
)}
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
|
||||
+16
-3
@@ -3,9 +3,22 @@ import { gql } from '@apollo/client';
|
||||
import { APPLICATION_REGISTRATION_FRAGMENT } from '@/settings/application-registrations/graphql/fragments/applicationRegistrationFragment';
|
||||
|
||||
export const FIND_ALL_APPLICATION_REGISTRATIONS = gql`
|
||||
query FindAllApplicationRegistrations {
|
||||
findAllApplicationRegistrations {
|
||||
...ApplicationRegistrationFragment
|
||||
query FindAllApplicationRegistrations(
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
$searchTerm: String
|
||||
$isPreInstalledOnly: Boolean
|
||||
) {
|
||||
findAllApplicationRegistrations(
|
||||
limit: $limit
|
||||
offset: $offset
|
||||
searchTerm: $searchTerm
|
||||
isPreInstalledOnly: $isPreInstalledOnly
|
||||
) {
|
||||
registrations {
|
||||
...ApplicationRegistrationFragment
|
||||
}
|
||||
hasMore
|
||||
}
|
||||
}
|
||||
${APPLICATION_REGISTRATION_FRAGMENT}
|
||||
|
||||
Reference in New Issue
Block a user