United group filter and group search (#3259)
This commit is contained in:
parent
bd9dbfb78b
commit
3287927797
10 changed files with 138 additions and 279 deletions
|
@ -65,7 +65,7 @@ describe("Group test", () => {
|
|||
.assertNoGroupsInThisRealmEmptyStateMessageExist(true)
|
||||
.createGroup(groupName, true)
|
||||
.assertNotificationGroupCreated()
|
||||
.searchGroup(groupName)
|
||||
.searchGroup(groupName, true)
|
||||
.assertGroupItemExist(groupName, true);
|
||||
});
|
||||
|
||||
|
@ -74,7 +74,7 @@ describe("Group test", () => {
|
|||
.assertNoGroupsInThisRealmEmptyStateMessageExist(false)
|
||||
.createGroup(groupName, false)
|
||||
.assertNotificationGroupCreated()
|
||||
.searchGroup(groupName)
|
||||
.searchGroup(groupName, true)
|
||||
.assertGroupItemExist(groupName, true);
|
||||
});
|
||||
|
||||
|
@ -84,7 +84,6 @@ describe("Group test", () => {
|
|||
.createGroup(" ", false)
|
||||
.assertNotificationCouldNotCreateGroupWithEmptyName();
|
||||
groupModal.closeModal();
|
||||
groupPage.searchGroup(" ").assertNoSearchResultsMessageExist(true);
|
||||
});
|
||||
|
||||
it("Fail to create group with duplicated name", () => {
|
||||
|
@ -115,10 +114,9 @@ describe("Group test", () => {
|
|||
|
||||
it("Delete group from item bar", () => {
|
||||
groupPage
|
||||
.searchGroup(groupNames[0])
|
||||
.searchGroup(groupNames[0], true)
|
||||
.deleteGroupItem(groupNames[0])
|
||||
.assertNotificationGroupDeleted()
|
||||
.searchGroup(groupNames[0])
|
||||
.assertNoSearchResultsMessageExist(true);
|
||||
});
|
||||
|
||||
|
@ -162,9 +160,7 @@ describe("Group test", () => {
|
|||
groupPage
|
||||
.goToGroupChildGroupsTab(predefinedGroups[0])
|
||||
.searchGroup(predefinedGroups[1])
|
||||
.goToGroupChildGroupsTab(predefinedGroups[1])
|
||||
.searchGroup(predefinedGroups[2])
|
||||
.assertGroupItemExist(predefinedGroups[2], true);
|
||||
.assertGroupItemExist(predefinedGroups[1], true);
|
||||
});
|
||||
|
||||
it("Search non existing child group in group", () => {
|
||||
|
@ -194,30 +190,7 @@ describe("Group test", () => {
|
|||
});
|
||||
|
||||
describe("Search globally", () => {
|
||||
it("Check empty state", () => {
|
||||
groupPage
|
||||
.headerActionsearchGroup()
|
||||
.assertNoSearchResultsMessageExist(true);
|
||||
});
|
||||
|
||||
it("Search with multiple words", () => {
|
||||
groupPage.headerActionsearchGroup();
|
||||
searchGroupPage
|
||||
.searchGroup(
|
||||
predefinedGroups[0].substr(0, predefinedGroups[0].length / 2)
|
||||
)
|
||||
.assertGroupItemExist(predefinedGroups[0], true)
|
||||
.searchGroup(
|
||||
predefinedGroups[0].substr(
|
||||
predefinedGroups[0].length / 2,
|
||||
predefinedGroups[0].length
|
||||
)
|
||||
)
|
||||
.assertGroupItemExist(predefinedGroups[0], true);
|
||||
});
|
||||
|
||||
it("Navigate to parent group details", () => {
|
||||
groupPage.headerActionsearchGroup();
|
||||
searchGroupPage
|
||||
.searchGroup(predefinedGroups[0])
|
||||
.goToGroupChildGroupsTab(predefinedGroups[0])
|
||||
|
@ -225,7 +198,6 @@ describe("Group test", () => {
|
|||
});
|
||||
|
||||
it("Navigate to sub-group details", () => {
|
||||
groupPage.headerActionsearchGroup();
|
||||
searchGroupPage
|
||||
.searchGroup(predefinedGroups[1])
|
||||
.goToGroupChildGroupsTab(predefinedGroups[1])
|
||||
|
|
|
@ -36,17 +36,8 @@ export default class GroupPage extends PageObject {
|
|||
return this;
|
||||
}
|
||||
|
||||
public searchGroup(groupName: string) {
|
||||
listingPage.searchItem(groupName, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public headerActionsearchGroup() {
|
||||
super.openDropdownMenu("", cy.findByTestId(this.actionDrpDwnButton));
|
||||
super.clickDropdownMenuItem(
|
||||
"",
|
||||
cy.findByTestId(this.actionDrpDwnItemSearchGroup)
|
||||
);
|
||||
public searchGroup(groupName: string, wait: boolean = false) {
|
||||
listingPage.searchItem(groupName, wait);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import GroupPage from "./GroupPage";
|
|||
|
||||
export class SearchGroupPage extends GroupPage {
|
||||
private searchField = "group-search";
|
||||
private searchButton = "search-button";
|
||||
private searchButton = "[data-testid='group-search'] > button";
|
||||
|
||||
public searchGroup(groupName: string) {
|
||||
this.typeSearchInput(groupName);
|
||||
|
@ -16,7 +16,7 @@ export class SearchGroupPage extends GroupPage {
|
|||
}
|
||||
|
||||
public clickSearchButton() {
|
||||
cy.findByTestId(this.searchButton).click();
|
||||
cy.get(this.searchButton).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
"createGroup": "Create group",
|
||||
"groupName": "Group name",
|
||||
"searchForGroups": "Search for groups",
|
||||
"searchFor": "Search for:",
|
||||
"global": "Global",
|
||||
"local": "Local",
|
||||
"searchGroups": "Search groups",
|
||||
"searchGroup": "Search group",
|
||||
"renameGroup": "Rename group",
|
||||
|
|
|
@ -2,7 +2,13 @@ import { useState } from "react";
|
|||
import { Link } from "react-router-dom-v5-compat";
|
||||
import { useLocation, useNavigate } from "react-router-dom-v5-compat";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cellWidth } from "@patternfly/react-table";
|
||||
import {
|
||||
Radio,
|
||||
SearchInput,
|
||||
Split,
|
||||
SplitItem,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
|
@ -19,11 +25,30 @@ import useToggle from "../utils/useToggle";
|
|||
import { DeleteGroup } from "./components/DeleteGroup";
|
||||
import { GroupToolbar, ViewType } from "./components/GroupToolbar";
|
||||
import { MoveDialog } from "./components/MoveDialog";
|
||||
import { GroupPath } from "../components/group/GroupPath";
|
||||
|
||||
type GroupTableProps = {
|
||||
toggleView?: (viewType: ViewType) => void;
|
||||
};
|
||||
|
||||
type SearchType = "global" | "local";
|
||||
|
||||
type SearchGroup = GroupRepresentation & {
|
||||
link?: string;
|
||||
};
|
||||
|
||||
const flatten = (groups: GroupRepresentation[], id?: string): SearchGroup[] => {
|
||||
let result: SearchGroup[] = [];
|
||||
for (const group of groups) {
|
||||
const link = `${id || ""}${id ? "/" : ""}${group.id}`;
|
||||
result.push({ ...group, link });
|
||||
if (group.subGroups) {
|
||||
result = [...result, ...flatten(group.subGroups, link)];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||
const { t } = useTranslation("groups");
|
||||
|
||||
|
@ -37,11 +62,15 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
const { subGroups, currentGroup, setSubGroups } = useSubGroups();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
const refresh = () => setKey(key + 1);
|
||||
const [search, setSearch] = useState<string>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const id = getLastId(location.pathname);
|
||||
const [searchType, setSearchType] = useState<SearchType>(
|
||||
id ? "local" : "global"
|
||||
);
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-users") || currentGroup()?.access?.manage;
|
||||
|
@ -49,7 +78,20 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
hasAccess("query-groups", "view-users") ||
|
||||
hasAccess("manage-users", "query-groups");
|
||||
|
||||
const loader = async () => {
|
||||
const loader = async (
|
||||
first?: number,
|
||||
max?: number
|
||||
): Promise<SearchGroup[]> => {
|
||||
const params: Record<string, string> = {
|
||||
search: search || "",
|
||||
first: first?.toString() || "",
|
||||
max: max?.toString() || "",
|
||||
};
|
||||
if (searchType === "global" && search) {
|
||||
const result = await fetchAdminUI(adminClient, "admin-ui-groups", params);
|
||||
return flatten(result);
|
||||
}
|
||||
|
||||
let groupsData = undefined;
|
||||
if (id) {
|
||||
const group = await adminClient.groups.findOne({ id });
|
||||
|
@ -57,9 +99,14 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
throw new Error(t("common:notFound"));
|
||||
}
|
||||
|
||||
groupsData = group.subGroups;
|
||||
groupsData = !search
|
||||
? group.subGroups
|
||||
: group.subGroups?.filter((g) => g.name?.includes(search));
|
||||
} else {
|
||||
groupsData = await fetchAdminUI(adminClient, "admin-ui-groups");
|
||||
groupsData = await fetchAdminUI(adminClient, "admin-ui-groups", {
|
||||
...params,
|
||||
global: "false",
|
||||
});
|
||||
}
|
||||
|
||||
if (!groupsData) {
|
||||
|
@ -92,6 +139,9 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
setIsCreateModalOpen(!isCreateModalOpen);
|
||||
};
|
||||
|
||||
const Path = (group: SearchGroup) =>
|
||||
group.link ? <GroupPath group={group} /> : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteGroup
|
||||
|
@ -109,8 +159,23 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
canSelectAll
|
||||
loader={loader}
|
||||
ariaLabelKey="groups:groups"
|
||||
searchPlaceholderKey="groups:searchForGroups"
|
||||
isPaginated
|
||||
isSearching={!!search}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<SearchInput
|
||||
data-testid="group-search"
|
||||
placeholder={t("searchForGroups")}
|
||||
value={search}
|
||||
onChange={setSearch}
|
||||
onSearch={refresh}
|
||||
onClear={() => {
|
||||
setSearch("");
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<GroupToolbar
|
||||
currentView={ViewType.Table}
|
||||
toggleView={toggleView}
|
||||
|
@ -118,6 +183,41 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
toggleDelete={toggleShowDelete}
|
||||
kebabDisabled={selectedRows!.length === 0}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
subToolbar={
|
||||
!!search &&
|
||||
!id && (
|
||||
<ToolbarItem>
|
||||
<Split hasGutter>
|
||||
<SplitItem>{t("searchFor")}</SplitItem>
|
||||
<SplitItem>
|
||||
<Radio
|
||||
id="global"
|
||||
isChecked={searchType === "global"}
|
||||
onChange={() => {
|
||||
setSearchType("global");
|
||||
refresh();
|
||||
}}
|
||||
name="searchType"
|
||||
label={t("global")}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Radio
|
||||
id="local"
|
||||
isChecked={searchType === "local"}
|
||||
onChange={() => {
|
||||
setSearchType("local");
|
||||
refresh();
|
||||
}}
|
||||
name="searchType"
|
||||
label={t("local")}
|
||||
/>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</ToolbarItem>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
!isManager
|
||||
|
@ -145,7 +245,11 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
|||
name: "name",
|
||||
displayKey: "groups:groupName",
|
||||
cellRenderer: GroupNameCell,
|
||||
transforms: [cellWidth(90)],
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
displayKey: "groups:path",
|
||||
cellRenderer: Path,
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom-v5-compat";
|
||||
import { useLocation, useNavigate } from "react-router-dom-v5-compat";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
|
@ -23,7 +22,6 @@ import { Members } from "./Members";
|
|||
import { GroupAttributes } from "./GroupAttributes";
|
||||
import { GroupsModal } from "./GroupsModal";
|
||||
import { toGroups } from "./routes/Groups";
|
||||
import { toGroupsSearch } from "./routes/GroupsSearch";
|
||||
import { GroupRoleMapping } from "./GroupRoleMapping";
|
||||
import helpUrls from "../help-urls";
|
||||
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
|
||||
|
@ -89,17 +87,6 @@ export default function GroupsSection() {
|
|||
[id]
|
||||
);
|
||||
|
||||
const SearchDropdown = (
|
||||
<DropdownItem
|
||||
key="searchGroup"
|
||||
component={
|
||||
<Link data-testid="searchGroup" to={toGroupsSearch({ realm })}>
|
||||
{t("searchGroup")}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteGroup
|
||||
|
@ -126,7 +113,6 @@ export default function GroupsSection() {
|
|||
dropdownItems={
|
||||
id && canManageGroup
|
||||
? [
|
||||
SearchDropdown,
|
||||
<DropdownItem
|
||||
data-testid="renameGroupAction"
|
||||
key="renameGroup"
|
||||
|
@ -142,7 +128,7 @@ export default function GroupsSection() {
|
|||
{t("deleteGroup")}
|
||||
</DropdownItem>,
|
||||
]
|
||||
: [SearchDropdown]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<PageSection variant={PageSectionVariants.light} className="pf-u-p-0">
|
||||
|
|
|
@ -1,183 +0,0 @@
|
|||
import { useState } from "react";
|
||||
import { Link } from "react-router-dom-v5-compat";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
InputGroup,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Stack,
|
||||
StackItem,
|
||||
TextInput,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { SearchIcon } from "@patternfly/react-icons";
|
||||
|
||||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { GroupPath } from "../components/group/GroupPath";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useAccess } from "../context/access/Access";
|
||||
|
||||
type SearchGroup = GroupRepresentation & {
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export default function SearchGroups() {
|
||||
const { t } = useTranslation("groups");
|
||||
const { adminClient } = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [searchTerms, setSearchTerms] = useState<string[]>([]);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
const isManager = hasAccess("manage-users", "query-clients");
|
||||
|
||||
const deleteTerm = (id: string) => {
|
||||
const index = searchTerms.indexOf(id);
|
||||
searchTerms.splice(index, 1);
|
||||
setSearchTerms([...searchTerms]);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const addTerm = () => {
|
||||
if (searchTerm !== "") {
|
||||
setSearchTerms([...searchTerms, searchTerm]);
|
||||
setSearchTerm("");
|
||||
refresh();
|
||||
}
|
||||
};
|
||||
|
||||
const GroupNameCell = (group: SearchGroup) => {
|
||||
if (!isManager) return <span>{group.name}</span>;
|
||||
|
||||
return (
|
||||
<Link key={group.id} to={`/${realm}/groups/search/${group.link}`}>
|
||||
{group.name}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const flatten = (
|
||||
groups: GroupRepresentation[],
|
||||
id?: string
|
||||
): SearchGroup[] => {
|
||||
let result: SearchGroup[] = [];
|
||||
for (const group of groups) {
|
||||
const link = `${id || ""}${id ? "/" : ""}${group.id}`;
|
||||
result.push({ ...group, link });
|
||||
if (group.subGroups) {
|
||||
result = [...result, ...flatten(group.subGroups, link)];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const loader = async (first?: number, max?: number) => {
|
||||
const params = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
};
|
||||
|
||||
let result: SearchGroup[] = [];
|
||||
if (searchTerms[0]) {
|
||||
result = await adminClient.groups.find({
|
||||
...params,
|
||||
search: searchTerms[0],
|
||||
});
|
||||
result = flatten(result);
|
||||
for (const searchTerm of searchTerms) {
|
||||
result = result.filter((group) => group.name?.includes(searchTerm));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const Path = (group: GroupRepresentation) => <GroupPath group={group} />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey="groups:searchGroups" />
|
||||
<PageSection variant={PageSectionVariants.light} className="pf-u-p-0">
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
isSearching
|
||||
toolbarItem={
|
||||
<ToolbarItem>
|
||||
<Stack>
|
||||
<StackItem className="pf-u-mb-sm">
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
name="search"
|
||||
data-testid="group-search"
|
||||
type="search"
|
||||
aria-label={t("search")}
|
||||
placeholder={t("searchGroups")}
|
||||
value={searchTerm}
|
||||
onChange={(value) => setSearchTerm(value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
addTerm();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
data-testid="search-button"
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("search")}
|
||||
onClick={addTerm}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</StackItem>
|
||||
<StackItem>
|
||||
<ChipGroup>
|
||||
{searchTerms.map((term) => (
|
||||
<Chip key={term} onClick={() => deleteTerm(term)}>
|
||||
{term}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</ToolbarItem>
|
||||
}
|
||||
ariaLabelKey="groups:groups"
|
||||
isPaginated
|
||||
loader={loader}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
displayKey: "groups:groupName",
|
||||
cellRenderer: GroupNameCell,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
displayKey: "groups:path",
|
||||
cellRenderer: Path,
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
hasIcon={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import type { RouteDef } from "../route-config";
|
||||
import { GroupsRoute, GroupsWithIdRoute } from "./routes/Groups";
|
||||
import { GroupsSearchRoute } from "./routes/GroupsSearch";
|
||||
|
||||
const routes: RouteDef[] = [GroupsSearchRoute, GroupsRoute, GroupsWithIdRoute];
|
||||
const routes: RouteDef[] = [GroupsRoute, GroupsWithIdRoute];
|
||||
|
||||
export default routes;
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { lazy } from "react";
|
||||
import type { Path } from "react-router-dom-v5-compat";
|
||||
import { generatePath } from "react-router-dom-v5-compat";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
|
||||
export type GroupsSearchParams = { realm: string };
|
||||
|
||||
export const GroupsSearchRoute: RouteDef = {
|
||||
path: "/:realm/groups/search",
|
||||
component: lazy(() => import("../SearchGroups")),
|
||||
breadcrumb: (t) => t("groups:searchGroups"),
|
||||
access: "query-groups",
|
||||
};
|
||||
|
||||
export const toGroupsSearch = (params: GroupsSearchParams): Partial<Path> => ({
|
||||
pathname: generatePath(GroupsSearchRoute.path, params),
|
||||
});
|
|
@ -53,11 +53,15 @@ public class GroupsResource {
|
|||
)}
|
||||
)
|
||||
public final Stream<GroupRepresentation> listGroups(@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first")
|
||||
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) {
|
||||
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("global") @DefaultValue("true") boolean global) {
|
||||
this.auth.groups().requireList();
|
||||
final Stream<GroupModel> stream;
|
||||
if ("".equals(search)) {
|
||||
if (!"".equals(search)) {
|
||||
if (global) {
|
||||
stream = this.realm.searchForGroupByNameStream(search, first, max);
|
||||
} else {
|
||||
stream = this.realm.getTopLevelGroupsStream().filter(g -> g.getName().contains(search)).skip(first).limit(max);
|
||||
}
|
||||
} else {
|
||||
stream = this.realm.getTopLevelGroupsStream(first, max);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue