initial commit
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { KTIcon } from "../../../../../../_digifi/helpers";
|
||||
import { useListView } from "../../core/ListViewProvider";
|
||||
import { UsersListFilter } from "./UsersListFilter";
|
||||
|
||||
const UsersListToolbar = () => {
|
||||
const { setItemIdForUpdate } = useListView();
|
||||
const openAddUserModal = () => {
|
||||
setItemIdForUpdate(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="d-flex justify-content-end"
|
||||
data-kt-user-table-toolbar="base"
|
||||
>
|
||||
<UsersListFilter />
|
||||
|
||||
{/* begin::Export */}
|
||||
{/* <button type='button' className='btn btn-light-primary me-3'>
|
||||
<KTIcon iconName='exit-up' className='fs-2' />
|
||||
Export
|
||||
</button> */}
|
||||
{/* end::Export */}
|
||||
|
||||
{/* begin::Add user */}
|
||||
{/* <button type='button' className='btn btn-primary' onClick={openAddUserModal}>
|
||||
<KTIcon iconName='plus' className='fs-2' />
|
||||
Add User
|
||||
</button> */}
|
||||
{/* end::Add user */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { UsersListToolbar };
|
||||
@@ -0,0 +1,136 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { MenuComponent } from "../../../../../../_digifi/assets/ts/components";
|
||||
import { initialQueryState, KTIcon } from "../../../../../../_digifi/helpers";
|
||||
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||
|
||||
const UsersListFilter = () => {
|
||||
const { updateState } = useQueryRequest();
|
||||
const { isLoading } = useQueryResponse();
|
||||
const [role, setRole] = useState<string | undefined>();
|
||||
const [lastLogin, setLastLogin] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
MenuComponent.reinitialization();
|
||||
}, []);
|
||||
|
||||
const resetData = () => {
|
||||
updateState({ filter: undefined, ...initialQueryState });
|
||||
};
|
||||
|
||||
const filterData = () => {
|
||||
updateState({
|
||||
filter: { role, last_login: lastLogin },
|
||||
...initialQueryState,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* begin::Filter Button */}
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
className="btn btn-light-primary me-3"
|
||||
data-kt-menu-trigger="click"
|
||||
data-kt-menu-placement="bottom-end"
|
||||
>
|
||||
<KTIcon iconName="filter" className="fs-2" />
|
||||
Filter
|
||||
</button>
|
||||
{/* end::Filter Button */}
|
||||
{/* begin::SubMenu */}
|
||||
<div
|
||||
className="menu menu-sub menu-sub-dropdown w-300px w-md-325px"
|
||||
data-kt-menu="true"
|
||||
>
|
||||
{/* begin::Header */}
|
||||
<div className="px-7 py-5">
|
||||
<div className="fs-5 text-gray-900 fw-bolder">Filter Options</div>
|
||||
</div>
|
||||
{/* end::Header */}
|
||||
|
||||
{/* begin::Separator */}
|
||||
<div className="separator border-gray-200"></div>
|
||||
{/* end::Separator */}
|
||||
|
||||
{/* begin::Content */}
|
||||
<div className="px-7 py-5" data-kt-user-table-filter="form">
|
||||
{/* begin::Input group */}
|
||||
<div className="mb-10">
|
||||
<label className="form-label fs-6 fw-bold">Role:</label>
|
||||
<select
|
||||
className="form-select form-select-solid fw-bolder"
|
||||
data-kt-select2="true"
|
||||
data-placeholder="Select option"
|
||||
data-allow-clear="true"
|
||||
data-kt-user-table-filter="role"
|
||||
data-hide-search="true"
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
value={role}
|
||||
>
|
||||
<option value=""></option>
|
||||
<option value="Administrator">Administrator</option>
|
||||
<option value="Analyst">Analyst</option>
|
||||
<option value="Developer">Developer</option>
|
||||
<option value="Support">Support</option>
|
||||
<option value="Trial">Trial</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Input group */}
|
||||
<div className="mb-10">
|
||||
<label className="form-label fs-6 fw-bold">Last login:</label>
|
||||
<select
|
||||
className="form-select form-select-solid fw-bolder"
|
||||
data-kt-select2="true"
|
||||
data-placeholder="Select option"
|
||||
data-allow-clear="true"
|
||||
data-kt-user-table-filter="two-step"
|
||||
data-hide-search="true"
|
||||
onChange={(e) => setLastLogin(e.target.value)}
|
||||
value={lastLogin}
|
||||
>
|
||||
<option value=""></option>
|
||||
<option value="Yesterday">Yesterday</option>
|
||||
<option value="20 mins ago">20 mins ago</option>
|
||||
<option value="5 hours ago">5 hours ago</option>
|
||||
<option value="2 days ago">2 days ago</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* end::Input group */}
|
||||
|
||||
{/* begin::Actions */}
|
||||
<div className="d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isLoading}
|
||||
onClick={filterData}
|
||||
className="btn btn-light btn-active-light-primary fw-bold me-2 px-6"
|
||||
data-kt-menu-dismiss="true"
|
||||
data-kt-user-table-filter="reset"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
disabled={isLoading}
|
||||
type="button"
|
||||
onClick={resetData}
|
||||
className="btn btn-primary fw-bold px-6"
|
||||
data-kt-menu-dismiss="true"
|
||||
data-kt-user-table-filter="filter"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
{/* end::Actions */}
|
||||
</div>
|
||||
{/* end::Content */}
|
||||
</div>
|
||||
{/* end::SubMenu */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export { UsersListFilter };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQueryClient, useMutation } from "react-query";
|
||||
import { QUERIES } from "../../../../../../_digifi/helpers";
|
||||
import { useListView } from "../../core/ListViewProvider";
|
||||
import { useQueryResponse } from "../../core/QueryResponseProvider";
|
||||
import { deleteSelectedUsers } from "../../core/_requests";
|
||||
|
||||
const UsersListGrouping = () => {
|
||||
const { selected, clearSelected } = useListView();
|
||||
const queryClient = useQueryClient();
|
||||
const { query } = useQueryResponse();
|
||||
|
||||
const deleteSelectedItems = useMutation(() => deleteSelectedUsers(selected), {
|
||||
// 💡 response of the mutation is passed to onSuccess
|
||||
onSuccess: () => {
|
||||
// ✅ update detail view directly
|
||||
queryClient.invalidateQueries([`${QUERIES.USERS_LIST}-${query}`]);
|
||||
clearSelected();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-end align-items-center">
|
||||
<div className="fw-bolder me-5">
|
||||
<span className="me-2">{selected.length}</span> Selected
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-danger"
|
||||
onClick={async () => await deleteSelectedItems.mutateAsync()}
|
||||
>
|
||||
Delete Selected
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { UsersListGrouping };
|
||||
@@ -0,0 +1,22 @@
|
||||
import {useListView} from '../../core/ListViewProvider'
|
||||
import {UsersListToolbar} from './UserListToolbar'
|
||||
import {UsersListGrouping} from './UsersListGrouping'
|
||||
import {UsersListSearchComponent} from './UsersListSearchComponent'
|
||||
|
||||
const UsersListHeader = () => {
|
||||
const {selected} = useListView()
|
||||
return (
|
||||
<div className='card-header border-0 pt-6'>
|
||||
<UsersListSearchComponent />
|
||||
{/* begin::Card toolbar */}
|
||||
<div className='card-toolbar'>
|
||||
{/* begin::Group actions */}
|
||||
{selected.length > 0 ? <UsersListGrouping /> : <UsersListToolbar />}
|
||||
{/* end::Group actions */}
|
||||
</div>
|
||||
{/* end::Card toolbar */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export {UsersListHeader}
|
||||
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
initialQueryState,
|
||||
KTIcon,
|
||||
useDebounce,
|
||||
} from "../../../../../../_digifi/helpers";
|
||||
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||
|
||||
const UsersListSearchComponent = () => {
|
||||
const { updateState } = useQueryRequest();
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
// Debounce search term so that it only gives us latest value ...
|
||||
// ... if searchTerm has not been updated within last 500ms.
|
||||
// The goal is to only have the API call fire when user stops typing ...
|
||||
// ... so that we aren't hitting our API rapidly.
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 150);
|
||||
// Effect for API call
|
||||
useEffect(
|
||||
() => {
|
||||
if (debouncedSearchTerm !== undefined && searchTerm !== undefined) {
|
||||
updateState({ search: debouncedSearchTerm, ...initialQueryState });
|
||||
}
|
||||
},
|
||||
[debouncedSearchTerm] // Only call effect if debounced search term changes
|
||||
// More details about useDebounce: https://usehooks.com/useDebounce/
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card-title">
|
||||
{/* begin::Search */}
|
||||
<div className="d-flex align-items-center position-relative my-1">
|
||||
<KTIcon iconName="magnifier" className="fs-1 position-absolute ms-6" />
|
||||
<input
|
||||
type="text"
|
||||
data-kt-user-table-filter="search"
|
||||
className="form-control form-control-solid w-250px ps-14"
|
||||
placeholder="Search user"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* end::Search */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { UsersListSearchComponent };
|
||||
@@ -0,0 +1,18 @@
|
||||
const UsersListLoading = () => {
|
||||
const styles = {
|
||||
borderRadius: '0.475rem',
|
||||
boxShadow: '0 0 50px 0 rgb(82 63 105 / 15%)',
|
||||
backgroundColor: '#fff',
|
||||
color: '#7e8299',
|
||||
fontWeight: '500',
|
||||
margin: '0',
|
||||
width: 'auto',
|
||||
padding: '1rem 2rem',
|
||||
top: 'calc(50% - 2rem)',
|
||||
left: 'calc(50% - 4rem)',
|
||||
}
|
||||
|
||||
return <div style={{...styles, position: 'absolute', textAlign: 'center'}}>Processing...</div>
|
||||
}
|
||||
|
||||
export {UsersListLoading}
|
||||
@@ -0,0 +1,179 @@
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
useQueryResponseLoading,
|
||||
useQueryResponsePagination,
|
||||
} from "../../core/QueryResponseProvider";
|
||||
import { useQueryRequest } from "../../core/QueryRequestProvider";
|
||||
import { PaginationState } from "../../../../../../_digifi/helpers";
|
||||
import { useMemo } from "react";
|
||||
|
||||
const mappedLabel = (label: string): string => {
|
||||
if (label === "« Previous") {
|
||||
return "Previous";
|
||||
}
|
||||
|
||||
if (label === "Next »") {
|
||||
return "Next";
|
||||
}
|
||||
|
||||
return label;
|
||||
};
|
||||
|
||||
const UsersListPagination = () => {
|
||||
const pagination = useQueryResponsePagination();
|
||||
const isLoading = useQueryResponseLoading();
|
||||
const { updateState } = useQueryRequest();
|
||||
const updatePage = (page: number | undefined | null) => {
|
||||
if (!page || isLoading || pagination.page === page) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateState({ page, items_per_page: pagination.items_per_page || 10 });
|
||||
};
|
||||
|
||||
const PAGINATION_PAGES_COUNT = 5;
|
||||
const sliceLinks = (pagination?: PaginationState) => {
|
||||
if (!pagination?.links?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const scopedLinks = [...pagination.links];
|
||||
|
||||
let pageLinks: Array<{
|
||||
label: string;
|
||||
active: boolean;
|
||||
url: string | null;
|
||||
page: number | null;
|
||||
}> = [];
|
||||
const previousLink: {
|
||||
label: string;
|
||||
active: boolean;
|
||||
url: string | null;
|
||||
page: number | null;
|
||||
} = scopedLinks.shift()!;
|
||||
const nextLink: {
|
||||
label: string;
|
||||
active: boolean;
|
||||
url: string | null;
|
||||
page: number | null;
|
||||
} = scopedLinks.pop()!;
|
||||
|
||||
const halfOfPagesCount = Math.floor(PAGINATION_PAGES_COUNT / 2);
|
||||
|
||||
pageLinks.push(previousLink);
|
||||
|
||||
if (
|
||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(0, PAGINATION_PAGES_COUNT),
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
pagination.page > scopedLinks.length - halfOfPagesCount &&
|
||||
scopedLinks.length > PAGINATION_PAGES_COUNT
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(
|
||||
scopedLinks.length - PAGINATION_PAGES_COUNT,
|
||||
scopedLinks.length
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
pagination.page <= Math.round(PAGINATION_PAGES_COUNT / 2) ||
|
||||
scopedLinks.length <= PAGINATION_PAGES_COUNT
|
||||
) &&
|
||||
!(pagination.page > scopedLinks.length - halfOfPagesCount)
|
||||
) {
|
||||
pageLinks = [
|
||||
...pageLinks,
|
||||
...scopedLinks.slice(
|
||||
pagination.page - 1 - halfOfPagesCount,
|
||||
pagination.page + halfOfPagesCount
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
pageLinks.push(nextLink);
|
||||
|
||||
return pageLinks;
|
||||
};
|
||||
|
||||
const paginationLinks = useMemo(() => sliceLinks(pagination), [pagination]);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12 col-md-5 d-flex align-items-center justify-content-center justify-content-md-start"></div>
|
||||
<div className="col-sm-12 col-md-7 d-flex align-items-center justify-content-center justify-content-md-end">
|
||||
<div id="kt_table_users_paginate">
|
||||
<ul className="pagination">
|
||||
<li
|
||||
className={clsx("page-item", {
|
||||
disabled: isLoading || pagination.page === 1,
|
||||
})}
|
||||
>
|
||||
<a
|
||||
onClick={() => updatePage(1)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="page-link"
|
||||
>
|
||||
First
|
||||
</a>
|
||||
</li>
|
||||
{paginationLinks
|
||||
?.map((link) => {
|
||||
return { ...link, label: mappedLabel(link.label) };
|
||||
})
|
||||
.map((link) => (
|
||||
<li
|
||||
key={link.label}
|
||||
className={clsx("page-item", {
|
||||
active: pagination.page === link.page,
|
||||
disabled: isLoading,
|
||||
previous: link.label === "Previous",
|
||||
next: link.label === "Next",
|
||||
})}
|
||||
>
|
||||
<a
|
||||
className={clsx("page-link", {
|
||||
"page-text":
|
||||
link.label === "Previous" || link.label === "Next",
|
||||
"me-5": link.label === "Previous",
|
||||
})}
|
||||
onClick={() => updatePage(link.page)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{mappedLabel(link.label)}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={clsx("page-item", {
|
||||
disabled:
|
||||
isLoading ||
|
||||
pagination.page === (pagination.links?.length || 3) - 2,
|
||||
})}
|
||||
>
|
||||
<a
|
||||
onClick={() => updatePage((pagination.links?.length || 3) - 2)}
|
||||
style={{ cursor: "pointer" }}
|
||||
className="page-link"
|
||||
>
|
||||
Last
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { UsersListPagination };
|
||||
Reference in New Issue
Block a user