Make filtering of client scopes same for clients and client scope section (#1064)

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2021-09-14 17:48:48 +02:00 committed by GitHub
parent 71d881ccd0
commit b8990cbf63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 421 additions and 245 deletions

View file

@ -1,67 +0,0 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
ButtonVariant,
Form,
Modal,
Radio,
} from "@patternfly/react-core";
import {
AllClientScopes,
AllClientScopeType,
allClientScopeTypes,
} from "../components/client-scope/ClientScopeTypes";
type ChangeTypeDialogProps = {
selectedClientScopes: number;
onConfirm: (scope: AllClientScopeType) => void;
onClose: () => void;
};
export const ChangeTypeDialog = ({
selectedClientScopes,
onConfirm,
onClose,
}: ChangeTypeDialogProps) => {
const { t } = useTranslation("client-scopes");
const [value, setValue] = useState<AllClientScopeType>(AllClientScopes.none);
return (
<Modal
title={t("changeType")}
isOpen={true}
onClose={onClose}
variant="small"
description={t("changeTypeIntro", { count: selectedClientScopes })}
actions={[
<Button
data-testid="change-scope-dialog-confirm"
key="confirm"
onClick={() => onConfirm(value)}
>
{t("common:continue")}
</Button>,
<Button key="cancel" variant={ButtonVariant.link} onClick={onClose}>
{t("common:cancel")}
</Button>,
]}
>
<Form isHorizontal>
{allClientScopeTypes.map((scope) => (
<Radio
key={scope}
isChecked={scope === value}
name={`radio-${scope}`}
onChange={(_val, event) => {
const { value } = event.currentTarget;
setValue(value as AllClientScopeType);
}}
label={t(`common:clientScope.${scope}`)}
id={`radio-${scope}`}
value={scope}
/>
))}
</Form>
</Modal>
);
};

View file

@ -0,0 +1,70 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertVariant, Select } from "@patternfly/react-core";
import {
allClientScopeTypes,
changeClientScope,
changeScope,
ClientScope,
clientScopeTypesSelectOptions,
} from "../components/client-scope/ClientScopeTypes";
import type { Row } from "../clients/scopes/ClientScopes";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
type ChangeTypeDropdownProps = {
clientId?: string;
selectedRows: Row[];
refresh: () => void;
};
export const ChangeTypeDropdown = ({
clientId,
selectedRows,
refresh,
}: ChangeTypeDropdownProps) => {
const { t } = useTranslation("client-scopes");
const [open, setOpen] = useState(false);
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
return (
<Select
id="change-type-dropdown"
isOpen={open}
selections={[]}
isDisabled={selectedRows.length === 0}
placeholderText={t("changeTypeTo")}
onToggle={(isExpanded) => setOpen(isExpanded)}
onSelect={async (_, value) => {
try {
await Promise.all(
selectedRows.map((row) => {
return clientId
? changeClientScope(
adminClient,
clientId,
row,
row.type,
value as ClientScope
)
: changeScope(adminClient, row, value as ClientScope);
})
);
setOpen(false);
refresh();
addAlert(t("clientScopeSuccess"), AlertVariant.success);
} catch (error) {
addError("clients:clientScopeError", error);
}
}}
>
{clientScopeTypesSelectOptions(
t,
!clientId ? allClientScopeTypes : undefined
)}
</Select>
);
};

View file

@ -28,12 +28,22 @@ import {
changeScope,
removeScope,
} from "../components/client-scope/ClientScopeTypes";
import { ChangeTypeDialog } from "./ChangeTypeDialog";
import { ChangeTypeDropdown } from "./ChangeTypeDropdown";
import { toNewClientScope } from "./routes/NewClientScope";
import "./client-scope.css";
import { toClientScope } from "./routes/ClientScope";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import {
nameFilter,
protocolFilter,
ProtocolType,
SearchDropdown,
SearchToolbar,
SearchType,
typeFilter,
} from "./details/SearchFilter";
import type { Row } from "../clients/scopes/ClientScopes";
export const ClientScopesSection = () => {
const { realm } = useRealm();
@ -47,21 +57,33 @@ export const ClientScopesSection = () => {
const refresh = () => setKey(new Date().getTime());
const [kebabOpen, setKebabOpen] = useState(false);
const [changeTypeOpen, setChangeTypeOpen] = useState(false);
const [selectedScopes, setSelectedScopes] = useState<
ClientScopeDefaultOptionalType[]
>([]);
const loader = async () => {
const [searchType, setSearchType] = useState<SearchType>("name");
const [searchTypeType, setSearchTypeType] = useState<AllClientScopes>(
AllClientScopes.none
);
const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("all");
const loader = async (first?: number, max?: number, search?: string) => {
const defaultScopes =
await adminClient.clientScopes.listDefaultClientScopes();
const optionalScopes =
await adminClient.clientScopes.listDefaultOptionalClientScopes();
const clientScopes = await adminClient.clientScopes.find();
const filter =
searchType === "name"
? nameFilter(search)
: searchType === "type"
? typeFilter(searchTypeType)
: protocolFilter(searchProtocol);
return clientScopes
.map((scope) => {
return {
const row: Row = {
...scope,
type: defaultScopes.find(
(defaultScope) => defaultScope.name === scope.name
@ -73,8 +95,11 @@ export const ClientScopesSection = () => {
? ClientScope.optional
: AllClientScopes.none,
};
return row;
})
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()));
.filter(filter)
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
.slice(first, max);
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
@ -138,24 +163,6 @@ export const ClientScopesSection = () => {
return (
<>
<DeleteConfirm />
{changeTypeOpen && (
<ChangeTypeDialog
selectedClientScopes={selectedScopes.length}
onConfirm={(type) => {
selectedScopes.map(async (scope) => {
try {
await changeScope(adminClient, scope, type);
addAlert(t("clientScopeSuccess"), AlertVariant.success);
refresh();
} catch (error) {
addError("client-scopes:clientScopeError", error);
}
});
setChangeTypeOpen(false);
}}
onClose={() => setChangeTypeOpen(false)}
/>
)}
<ViewHeader
titleKey="clientScopes"
subKey="client-scopes:clientScopeExplain"
@ -165,17 +172,49 @@ export const ClientScopesSection = () => {
key={key}
loader={loader}
ariaLabelKey="client-scopes:clientScopeList"
searchPlaceholderKey="client-scopes:searchFor"
searchPlaceholderKey={
searchType === "name" ? "client-scopes:searchFor" : undefined
}
isSearching={searchType !== "name"}
searchTypeComponent={
<SearchDropdown
searchType={searchType}
onSelect={(searchType) => setSearchType(searchType)}
withProtocol
/>
}
isPaginated
onSelect={(clientScopes) => setSelectedScopes([...clientScopes])}
canSelectAll
toolbarItem={
<>
<SearchToolbar
searchType={searchType}
type={searchTypeType}
onSelect={(searchType) => setSearchType(searchType)}
onType={(value) => {
setSearchTypeType(value);
refresh();
}}
protocol={searchProtocol}
onProtocol={(protocol) => {
setSearchProtocol(protocol);
refresh();
}}
/>
<ToolbarItem>
{/* @ts-ignore */}
<Button component={Link} to={toNewClientScope({ realm })}>
{t("createClientScope")}
</Button>
</ToolbarItem>
<ToolbarItem>
<ChangeTypeDropdown
selectedRows={selectedScopes}
refresh={refresh}
/>
</ToolbarItem>
<ToolbarItem>
<Dropdown
toggle={
@ -184,18 +223,6 @@ export const ClientScopesSection = () => {
isOpen={kebabOpen}
isPlain
dropdownItems={[
<DropdownItem
key="changeType"
component="button"
isDisabled={selectedScopes.length === 0}
onClick={() => {
setChangeTypeOpen(true);
setKebabOpen(false);
}}
>
{t("changeType")}
</DropdownItem>,
<DropdownItem
key="action"
component="button"

View file

@ -0,0 +1,164 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import {
Dropdown,
DropdownItem,
DropdownToggle,
Select,
SelectOption,
ToolbarItem,
} from "@patternfly/react-core";
import { FilterIcon } from "@patternfly/react-icons";
import {
AllClientScopes,
AllClientScopeType,
clientScopeTypesSelectOptions,
} from "../../components/client-scope/ClientScopeTypes";
import type { Row } from "../../clients/scopes/ClientScopes";
export type SearchType = "name" | "type" | "protocol";
export const PROTOCOLS = ["all", "saml", "openid-connect"] as const;
export type ProtocolType = typeof PROTOCOLS[number];
export const nameFilter =
(search = "") =>
(scope: Row) =>
scope.name?.includes(search);
export const typeFilter = (type: AllClientScopeType) => (scope: Row) =>
type === AllClientScopes.none || scope.type === type;
export const protocolFilter = (protocol: ProtocolType) => (scope: Row) =>
protocol === "all" || scope.protocol === protocol;
type SearchToolbarProps = Omit<SearchDropdownProps, "withProtocol"> & {
type: AllClientScopeType;
onType: (value: AllClientScopes) => void;
protocol?: ProtocolType;
onProtocol?: (value: ProtocolType) => void;
};
type SearchDropdownProps = {
searchType: SearchType;
onSelect: (value: SearchType) => void;
withProtocol?: boolean;
};
export const SearchDropdown = ({
searchType,
withProtocol = false,
onSelect,
}: SearchDropdownProps) => {
const { t } = useTranslation("clients");
const [searchToggle, setSearchToggle] = useState(false);
const createDropdown = (searchType: SearchType) => (
<DropdownItem
key={searchType}
onClick={() => {
onSelect(searchType);
setSearchToggle(false);
}}
>
{t(`clientScopeSearch.${searchType}`)}
</DropdownItem>
);
const options = [createDropdown("name"), createDropdown("type")];
if (withProtocol) {
options.push(createDropdown("protocol"));
}
return (
<Dropdown
className="keycloak__client-scopes__searchtype"
toggle={
<DropdownToggle
id="toggle-id"
onToggle={(open) => setSearchToggle(open)}
>
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
</DropdownToggle>
}
isOpen={searchToggle}
dropdownItems={options}
/>
);
};
export const SearchToolbar = ({
searchType,
onSelect,
type,
onType,
protocol,
onProtocol,
}: SearchToolbarProps) => {
const { t } = useTranslation("client-scopes");
const [open, setOpen] = useState(false);
return (
<>
{searchType === "type" && (
<>
<ToolbarItem>
<SearchDropdown
searchType={searchType}
onSelect={onSelect}
withProtocol={!!protocol}
/>
</ToolbarItem>
<ToolbarItem>
<Select
className="keycloak__client-scopes__searchtype"
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={[
type === AllClientScopes.none
? t("common:allTypes")
: t(`common:clientScope.${type}`),
]}
onSelect={(_, value) => {
onType(value as AllClientScopes);
setOpen(false);
}}
>
<SelectOption value={AllClientScopes.none}>
{t("common:allTypes")}
</SelectOption>
<>{clientScopeTypesSelectOptions(t)}</>
</Select>
</ToolbarItem>
</>
)}
{searchType === "protocol" && !!protocol && (
<>
<ToolbarItem>
<SearchDropdown
searchType={searchType}
onSelect={onSelect}
withProtocol
/>
</ToolbarItem>
<ToolbarItem>
<Select
className="keycloak__client-scopes__searchtype"
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={[t(`protocolTypes.${protocol}`)]}
onSelect={(_, value) => {
onProtocol?.(value as ProtocolType);
setOpen(false);
}}
>
{PROTOCOLS.map((type) => (
<SelectOption key={type} value={type}>
{t(`protocolTypes.${type}`)}
</SelectOption>
))}
</Select>
</ToolbarItem>
</>
)}
</>
);
};

View file

@ -14,7 +14,7 @@ export default {
deleteClientScope: "Delete client scope {{name}}",
deleteClientScope_plural: "Delete {{count}} client scopes",
deleteConfirm: "Are you sure you want to delete this client scope",
changeType: "Change type",
changeTypeTo: "Change type to",
changeTypeIntro: "{{count}} selected client scopes will be changed to",
clientScopeSuccess: "Scope mapping updated",
clientScopeError: "Could not update scope mapping {{error}}",
@ -62,5 +62,10 @@ export default {
scope: "Scope",
roleMappingUpdatedSuccess: "Role mapping updated",
roleMappingUpdatedError: "Could not update role mapping {{error}}",
protocolTypes: {
all: "All",
saml: "SAML",
"openid-connect": "openid-connect",
},
},
};

View file

@ -30,7 +30,6 @@ export default {
searchByName: "Search by name",
setup: "Setup",
evaluate: "Evaluate",
changeTypeTo: "Change type to",
assignRole: "Assign role",
unAssignRole: "Unassign",
removeMappingTitle: "Remove mapping?",
@ -38,8 +37,9 @@ export default {
removeMappingConfirm_plural:
"Are you sure you want to remove {{count}} mappings",
clientScopeSearch: {
client: "Client scope",
assigned: "Assigned type",
name: "Name",
type: "Assigned type",
protocol: "Protocol",
},
assignedClientScope: "Assigned client scope",
assignedType: "Assigned type",

View file

@ -5,92 +5,57 @@ import {
Button,
Dropdown,
DropdownItem,
DropdownToggle,
KebabToggle,
Select,
ToolbarItem,
} from "@patternfly/react-core";
import { FilterIcon } from "@patternfly/react-icons";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import type KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import { useAdminClient } from "../../context/auth/AdminClient";
import { toUpperCase } from "../../util";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { AddScopeDialog } from "./AddScopeDialog";
import {
clientScopeTypesSelectOptions,
ClientScopeType,
ClientScope,
CellDropdown,
AllClientScopes,
AllClientScopeType,
changeClientScope,
addClientScope,
removeClientScope,
} from "../../components/client-scope/ClientScopeTypes";
import { useAlerts } from "../../components/alert/Alerts";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import {
nameFilter,
SearchDropdown,
SearchToolbar,
SearchType,
typeFilter,
} from "../../client-scopes/details/SearchFilter";
import "./client-scopes.css";
import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown";
export type ClientScopesProps = {
clientId: string;
protocol: string;
};
type Row = ClientScopeRepresentation & {
type: ClientScopeType;
description: string;
export type Row = ClientScopeRepresentation & {
type: AllClientScopeType;
description?: string;
};
const castAdminClient = (adminClient: KeycloakAdminClient) =>
adminClient.clients as unknown as {
[index: string]: Function;
};
const changeScope = async (
adminClient: KeycloakAdminClient,
clientId: string,
clientScope: ClientScopeRepresentation,
type: ClientScopeType,
changeTo: ClientScopeType
) => {
await removeScope(adminClient, clientId, clientScope, type);
await addScope(adminClient, clientId, clientScope, changeTo);
};
const removeScope = async (
adminClient: KeycloakAdminClient,
clientId: string,
clientScope: ClientScopeRepresentation,
type: ClientScopeType
) => {
const typeToName = toUpperCase(type);
await castAdminClient(adminClient)[`del${typeToName}ClientScope`]({
id: clientId,
clientScopeId: clientScope.id!,
});
};
const addScope = async (
adminClient: KeycloakAdminClient,
clientId: string,
clientScope: ClientScopeRepresentation,
type: ClientScopeType
) => {
const typeToName = toUpperCase(type);
await castAdminClient(adminClient)[`add${typeToName}ClientScope`]({
id: clientId,
clientScopeId: clientScope.id!,
});
};
type SearchType = "client" | "assigned";
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const [searchToggle, setSearchToggle] = useState(false);
const [searchType, setSearchType] = useState<SearchType>("client");
const [addToggle, setAddToggle] = useState(false);
const [searchType, setSearchType] = useState<SearchType>("name");
const [searchTypeType, setSearchTypeType] = useState<AllClientScopes>(
AllClientScopes.none
);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [kebabOpen, setKebabOpen] = useState(false);
@ -100,7 +65,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const loader = async () => {
const loader = async (first?: number, max?: number, search?: string) => {
const defaultClientScopes =
await adminClient.clients.listDefaultClientScopes({ id: clientId });
const optionalClientScopes =
@ -112,20 +77,22 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const optional = optionalClientScopes.map((c) => {
const scope = find(c.id!);
return {
const row: Row = {
...c,
type: ClientScope.optional,
description: scope.description,
} as Row;
};
return row;
});
const defaultScopes = defaultClientScopes.map((c) => {
const scope = find(c.id!);
return {
const row: Row = {
...c,
type: ClientScope.default,
description: scope.description,
} as Row;
};
return row;
});
const rows = [...optional, ...defaultScopes];
@ -136,7 +103,9 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
.filter((scope) => scope.protocol === protocol)
);
return rows;
const filter =
searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType);
return rows.filter(filter).slice(first, max);
};
const TypeSelector = (scope: Row) => (
@ -145,7 +114,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
type={scope.type}
onSelect={async (value) => {
try {
await changeScope(
await changeClientScope(
adminClient,
clientId,
scope,
@ -173,7 +142,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
await Promise.all(
scopes.map(
async (scope) =>
await addScope(
await addClientScope(
adminClient,
clientId,
scope.scope,
@ -194,83 +163,41 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
key={key}
loader={loader}
ariaLabelKey="clients:clientScopeList"
searchPlaceholderKey="clients:searchByName"
searchPlaceholderKey={
searchType === "name" ? "clients:searchByName" : undefined
}
canSelectAll
isPaginated
isSearching={searchType === "type"}
onSelect={(rows) => setSelectedRows([...rows])}
searchTypeComponent={
<Dropdown
className="keycloak__client-scopes__searchtype"
toggle={
<DropdownToggle
id="toggle-id"
onToggle={() => setSearchToggle(!searchToggle)}
>
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
</DropdownToggle>
}
aria-label="Select Input"
isOpen={searchToggle}
dropdownItems={[
<DropdownItem
key="client"
onClick={() => {
setSearchType("client");
setSearchToggle(false);
}}
>
{t("clientScopeSearch.client")}
</DropdownItem>,
<DropdownItem
key="assigned"
onClick={() => {
setSearchType("assigned");
setSearchToggle(false);
}}
>
{t("clientScopeSearch.assigned")}
</DropdownItem>,
]}
<SearchDropdown
searchType={searchType}
onSelect={(searchType) => setSearchType(searchType)}
/>
}
toolbarItem={
<>
<SearchToolbar
searchType={searchType}
type={searchTypeType}
onSelect={(searchType) => setSearchType(searchType)}
onType={(value) => {
setSearchTypeType(value);
refresh();
}}
/>
<ToolbarItem>
<Button onClick={() => setAddDialogOpen(true)}>
{t("addClientScope")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Select
id="add-dropdown"
key="add-dropdown"
isOpen={addToggle}
selections={[]}
isDisabled={selectedRows.length === 0}
placeholderText={t("changeTypeTo")}
onToggle={() => setAddToggle(!addToggle)}
onSelect={async (_, value) => {
try {
await Promise.all(
selectedRows.map((row) => {
return changeScope(
adminClient,
clientId,
{ ...row },
row.type,
value as ClientScope
);
})
);
setAddToggle(false);
refresh();
addAlert(t("clientScopeSuccess"), AlertVariant.success);
} catch (error) {
addError("clients:clientScopeError", error);
}
}}
>
{clientScopeTypesSelectOptions(t)}
</Select>
<ChangeTypeDropdown
clientId={clientId}
selectedRows={selectedRows}
refresh={refresh}
/>
</ToolbarItem>
<ToolbarItem>
<Dropdown
@ -287,11 +214,11 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
try {
await Promise.all(
selectedRows.map(async (row) => {
await removeScope(
await removeClientScope(
adminClient,
clientId,
{ ...row },
row.type
row.type as ClientScope
);
})
);
@ -331,7 +258,12 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
title: t("common:remove"),
onRowClick: async (row) => {
try {
await removeScope(adminClient, clientId, row, row.type);
await removeClientScope(
adminClient,
clientId,
row,
row.type as ClientScope
);
addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success);
refresh();
} catch (error) {

View file

@ -65,6 +65,7 @@ export default {
optional: "Optional",
none: "None",
},
allTypes: "All types",
home: "Home",
manage: "Manage",

View file

@ -6,6 +6,7 @@ import { DropdownItem, Select, SelectOption } from "@patternfly/react-core";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import type KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import { toUpperCase } from "../../util";
export enum ClientScope {
default = "default",
@ -127,3 +128,44 @@ const addScope = async (
id: clientScope.id!,
});
};
export const changeClientScope = async (
adminClient: KeycloakAdminClient,
clientId: string,
clientScope: ClientScopeRepresentation,
type: AllClientScopeType,
changeTo: ClientScopeType
) => {
if (type !== "none") {
await removeClientScope(adminClient, clientId, clientScope, type);
}
await addClientScope(adminClient, clientId, clientScope, changeTo);
};
export const removeClientScope = async (
adminClient: KeycloakAdminClient,
clientId: string,
clientScope: ClientScopeRepresentation,
type: ClientScope
) => {
const methodName = `del${toUpperCase(type)}ClientScope` as const;
await adminClient.clients[methodName]({
id: clientId,
clientScopeId: clientScope.id!,
});
};
export const addClientScope = async (
adminClient: KeycloakAdminClient,
clientId: string,
clientScope: ClientScopeRepresentation,
type: ClientScopeType
) => {
const methodName = `add${toUpperCase(type)}ClientScope` as const;
await adminClient.clients[methodName]({
id: clientId,
clientScopeId: clientScope.id!,
});
};

View file

@ -413,9 +413,7 @@ export function KeycloakDataTable<T>({
)}
{!loading &&
(!data || data.length === 0) &&
search !== "" &&
!isSearching &&
searchPlaceholderKey && (
(search !== "" || isSearching) && (
<ListEmptyState
hasIcon={true}
icon={icon}
@ -427,7 +425,11 @@ export function KeycloakDataTable<T>({
{loading && <Loading />}
</PaginatingTableToolbar>
)}
{!loading && (!data || data?.length === 0) && search === "" && emptyState}
{!loading &&
(!data || data?.length === 0) &&
search === "" &&
!isSearching &&
emptyState}
</>
);
}

View file

@ -51,8 +51,8 @@ export const exportClient = (client: ClientRepresentation): void => {
);
};
export const toUpperCase = (name: string) =>
name.charAt(0).toUpperCase() + name.slice(1);
export const toUpperCase = <T extends string>(name: T) =>
(name.charAt(0).toUpperCase() + name.slice(1)) as Capitalize<T>;
export const convertToFormValues = (
obj: any,