gitscope
← marzneshin / Diff · 7738ec1 → 7738ec1
Changes 7738ec1..7738ec1
Patch commit
diff --git a/dashboard/package.json b/dashboard/package.json
index 95caefd..f13ab00 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -99,6 +99,7 @@
"sonner": "^1.4.41",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
+ "use-debounce": "^10.0.3",
"vaul": "^0.9.1",
"yaml": "^2.5.0",
"zod": "^3.23.8",
diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml
index 0244734..73a8fd6 100644
--- a/dashboard/pnpm-lock.yaml
+++ b/dashboard/pnpm-lock.yaml
@@ -248,6 +248,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.3)
+ use-debounce:
+ specifier: ^10.0.3
+ version: 10.0.3(react@18.3.1)
vaul:
specifier: ^0.9.1
version: 0.9.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -8487,6 +8490,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ use-debounce@10.0.3:
+ resolution: {integrity: sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg==}
+ engines: {node: '>= 16.0.0'}
+ peerDependencies:
+ react: '*'
+
use-deep-compare-effect@1.8.1:
resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==}
engines: {node: '>=10', npm: '>=6'}
@@ -19956,6 +19965,10 @@ snapshots:
dependencies:
react: 18.3.1
+ use-debounce@10.0.3(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
use-deep-compare-effect@1.8.1(react@18.3.1):
dependencies:
'@babel/runtime': 7.24.6
diff --git a/dashboard/src/features/admins/types/admin-prop.ts b/dashboard/src/features/admins/types/admin-prop.ts
deleted file mode 100644
index 05220ad..0000000
--- a/dashboard/src/features/admins/types/admin-prop.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { AdminType } from "@marzneshin/features/admins";
-
-export interface AdminProp {
- admin: AdminType;
-}
diff --git a/dashboard/src/features/entity-table/components/column-header-combo-filter.tsx b/dashboard/src/features/entity-table/components/column-header-combo-filter.tsx
index 8ac74e4..44eebd7 100644
--- a/dashboard/src/features/entity-table/components/column-header-combo-filter.tsx
+++ b/dashboard/src/features/entity-table/components/column-header-combo-filter.tsx
@@ -36,18 +36,14 @@ export function DataTableColumnHeaderFilterOption<TData, TValue>(
const [selectedOption, setSelectedOption] = React.useState<string | null>(
null
)
- const { filters, table } = useEntityTableContext();
+ const { filters, table: { setPageIndex } } = useEntityTableContext();
function handleClearingFilter() {
filters.setColumnsFilter({ ...filters.columnsFilter, [column.id]: undefined });
setSelectedOption(null);
+ setPageIndex(1);
}
- React.useEffect(() => {
- // if (table.getState().pagination.pageIndex !== 1)
- table.setPageIndex(1);
- }, [table, selectedOption])
-
if (isDesktop) {
return (
<Popover open={open} onOpenChange={setOpen}>
@@ -69,7 +65,7 @@ export function DataTableColumnHeaderFilterOption<TData, TValue>(
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0" align="start">
- <ComboFilterOptionList setOpen={setOpen} columnName={column.id} setSelectedOption={setSelectedOption} options={options} />
+ <ComboFilterOptionList setOpen={setOpen} columnName={column.id} setSelectedOption={(value) => { setSelectedOption(value); setPageIndex(1); }} options={options} />
</PopoverContent>
</Popover>
)
diff --git a/dashboard/src/features/entity-table/components/data-table.tsx b/dashboard/src/features/entity-table/components/data-table.tsx
index 092baa2..5823893 100644
--- a/dashboard/src/features/entity-table/components/data-table.tsx
+++ b/dashboard/src/features/entity-table/components/data-table.tsx
@@ -5,7 +5,6 @@ import {
RowSelectionState,
SortingState,
VisibilityState,
- flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
@@ -15,19 +14,14 @@ import {
import {
Button,
- Input,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
} from "@marzneshin/components"
import { useTranslation } from "react-i18next"
import { DataTablePagination } from "./table-pagination"
import { useState } from "react"
import { DataTableViewOptions } from "./table-view-option"
+import { TableSearch } from "./table-filtering"
+import { EntityDataTable } from "./table";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
@@ -44,7 +38,6 @@ export function DataTable<TData, TValue>({
data,
onCreate,
onOpen,
- filteredColumn,
selectedRow,
setSelectedRow,
}: DataTableProps<TData, TValue>) {
@@ -76,61 +69,12 @@ export function DataTable<TData, TValue>({
return (
<div>
<div className="flex items-center py-4">
- <Input
- placeholder={t('table.filter-placeholder', { name: filteredColumn })}
- value={(table.getColumn(filteredColumn)?.getFilterValue() as string) ?? ""}
- onChange={(event) =>
- table.getColumn(filteredColumn)?.setFilterValue(event.target.value)
- }
- className="max-w-sm"
- />
+ <TableSearch />
<DataTableViewOptions table={table} />
{onCreate && (<Button onClick={onCreate}>{t('create')}</Button>)}
</div>
<div className="rounded-md border">
- <Table>
- <TableHeader>
- {table.getHeaderGroups().map((headerGroup) => (
- <TableRow key={headerGroup.id}>
- {headerGroup.headers.map((header) => {
- return (
- <TableHead key={header.id}>
- {header.isPlaceholder
- ? null
- : flexRender(
- header.column.columnDef.header,
- header.getContext()
- )}
- </TableHead>
- )
- })}
- </TableRow>
- ))}
- </TableHeader>
- <TableBody>
- {table.getRowModel().rows?.length ? (
- table.getRowModel().rows.map((row) => (
- <TableRow
- key={row.id}
- data-state={row.getIsSelected() && "selected"}
- onClick={() => onOpen !== undefined && onOpen(row.original)}
- >
- {row.getVisibleCells().map((cell) => (
- <TableCell key={cell.id}>
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
- </TableCell>
- ))}
- </TableRow>
- ))
- ) : (
- <TableRow>
- <TableCell colSpan={columns.length} className="h-24 text-center">
- {t('table.no-result')}
- </TableCell>
- </TableRow>
- )}
- </TableBody>
- </Table>
+ <EntityDataTable columns={columns} onRowClick={onOpen} />
<DataTablePagination table={table} />
</div>
</div>
diff --git a/dashboard/src/features/entity-table/components/table-filtering.tsx b/dashboard/src/features/entity-table/components/table-filtering.tsx
index bc2865d..2b63c16 100644
--- a/dashboard/src/features/entity-table/components/table-filtering.tsx
+++ b/dashboard/src/features/entity-table/components/table-filtering.tsx
@@ -1,21 +1,33 @@
-
-import { type FC } from "react";
+import { type FC, useEffect, useState } from "react";
import { Input } from "@marzneshin/components";
import {
useEntityTableContext
} from "@marzneshin/features/entity-table/contexts";
import { useTranslation } from "react-i18next";
+import { useDebouncedCallback } from 'use-debounce';
interface TableSearchProps { }
export const TableSearch: FC<TableSearchProps> = () => {
const { primaryFilter } = useEntityTableContext()
+ const [keyword, setKeyword] = useState(primaryFilter.columnFilters)
+
+ const setColumnFilters = useDebouncedCallback(
+ primaryFilter.setColumnFilters,
+ 500
+ );
+
+ useEffect(() => {
+ setColumnFilters(keyword);
+ }, [keyword, setColumnFilters])
+
const { t } = useTranslation()
+
return (
<Input
placeholder={t('table.filter-placeholder', { name: primaryFilter.column })}
- value={primaryFilter.columnFilters}
- onChange={(e) => primaryFilter.setColumnFilters(e.target.value)}
+ value={keyword}
+ onChange={(e) => setKeyword(e.target.value)}
className=" w-full md:w-[20rem]"
/>
);
diff --git a/dashboard/src/features/entity-table/components/table.tsx b/dashboard/src/features/entity-table/components/table.tsx
index b44e42f..6b8b47e 100644
--- a/dashboard/src/features/entity-table/components/table.tsx
+++ b/dashboard/src/features/entity-table/components/table.tsx
@@ -42,7 +42,7 @@ const Rows: FC<Readonly<DataTableProps<any, any>>> = ({
const { table } = useEntityTableContext();
const { t } = useTranslation();
- return (table.getRowModel().rows.length ? (
+ return (table.getRowModel().rows?.length ? (
table.getRowModel().rows.map(row => (
<TableRow
key={row.id}
@@ -66,36 +66,36 @@ const Rows: FC<Readonly<DataTableProps<any, any>>> = ({
))
};
+const Loading = () => (
+ <>
+ {Array.from({ length: 5 }).map((_, rowIndex) => (
+ <TableRow key={`skeleton-row-${rowIndex}`} className="w-full">
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ <TableCell className="h-12">
+ <Skeleton className="w-full h-full" />
+ </TableCell>
+ </TableRow>
+ ))}
+ </>
+);
+
export function EntityDataTable<TData, TValue>({
columns,
onRowClick,
}: Readonly<DataTableProps<TData, TValue>>) {
const { isLoading } = useEntityTableContext();
- const Loading = () => (
- <>
- {Array.from({ length: 5 }).map((_, rowIndex) => (
- <TableRow key={`skeleton-row-${rowIndex}`} className="w-full">
- <TableCell className="h-12">
- <Skeleton className="w-full h-full" />
- </TableCell>
- <TableCell className="h-12">
- <Skeleton className="w-full h-full" />
- </TableCell>
- <TableCell className="h-12">
- <Skeleton className="w-full h-full" />
- </TableCell>
- <TableCell className="h-12">
- <Skeleton className="w-full h-full" />
- </TableCell>
- <TableCell className="h-12">
- <Skeleton className="w-full h-full" />
- </TableCell>
- </TableRow>
- ))}
- </>
- );
-
return (
<Table className="w-full">
<TableHeader> <Headers /> </TableHeader>
diff --git a/dashboard/src/features/entity-table/double-entity-table.tsx b/dashboard/src/features/entity-table/double-entity-table.tsx
new file mode 100644
index 0000000..c97ac50
--- /dev/null
+++ b/dashboard/src/features/entity-table/double-entity-table.tsx
@@ -0,0 +1,108 @@
+import { useMemo } from "react";
+import { Button } from "@marzneshin/components";
+import { DataTableViewOptions } from "./components";
+import { useTranslation } from "react-i18next";
+import { useQuery } from "@tanstack/react-query";
+import { ColumnDef } from "@tanstack/react-table";
+import { EntityTableContext } from "./contexts";
+import { TableSearch, DataTablePagination, EntityDataTable } from "./components";
+import {
+ usePrimaryFiltering,
+ usePagination,
+ useEntityTable,
+ useVisibility,
+ useSorting,
+ type UseRowSelectionReturn,
+ type FetchEntityReturn,
+ type DoubleEntityQueryKey,
+ type DoubleEntityQueryKeyType,
+ useFilters,
+} from "./hooks";
+
+export interface DoubleEntityTableProps<T> {
+ columns: ColumnDef<T>[];
+ primaryFilter: string;
+ entityKey: string;
+ entityId: number;
+ rowSelection?: UseRowSelectionReturn;
+ fetchEntity: ({ queryKey }: DoubleEntityQueryKeyType) => FetchEntityReturn<T>;
+ onCreate?: () => void;
+ onOpen?: (entity: any) => void;
+}
+
+export function DoubleEntityTable<T>({
+ fetchEntity,
+ columns,
+ primaryFilter,
+ rowSelection,
+ entityKey,
+ entityId,
+ onCreate,
+ onOpen,
+}: DoubleEntityTableProps<T>) {
+ const { t } = useTranslation();
+ const columnPrimaryFilter = usePrimaryFiltering({ column: primaryFilter });
+ const filters = useFilters();
+ const sorting = useSorting();
+ const visibility = useVisibility();
+ const { onPaginationChange, pageIndex, pageSize } = usePagination();
+
+ const query: DoubleEntityQueryKey = [
+ entityKey,
+ entityId,
+ {
+ page: pageIndex,
+ size: pageSize,
+ },
+ columnPrimaryFilter.columnFilters,
+ {
+ sortBy: sorting.sorting[0]?.id || "created_at",
+ desc: sorting.sorting[0]?.desc || true,
+ },
+ { filters: filters.columnsFilter }
+ ];
+
+ const { data, isFetching } = useQuery({
+ queryFn: fetchEntity,
+ queryKey: query,
+ initialData: { entities: [], pageCount: 1 },
+ });
+
+ const table = useEntityTable({
+ data,
+ columns,
+ pageSize,
+ pageIndex,
+ rowSelection,
+ visibility,
+ sorting,
+ onPaginationChange,
+ });
+
+ const contextValue = useMemo(
+ () => ({ table, data: data.entities, primaryFilter: columnPrimaryFilter, filters, isLoading: isFetching }),
+ [table, data.entities, filters, columnPrimaryFilter, isFetching],
+ );
+
+ return (
+ <EntityTableContext.Provider value={contextValue}>
+ <div className="flex flex-col">
+ <div className="flex flex-col md:flex-row-reverse items-center py-4 gap-2 w-full">
+ <div className="flex flex-row items-center w-full">
+ <DataTableViewOptions table={table} />
+ {onCreate && (
+ <Button aria-label={`create-${entityKey}`} onClick={onCreate}>
+ {t("create")}
+ </Button>
+ )}
+ </div>
+ <TableSearch />
+ </div>
+ <div className="w-full rounded-md border">
+ <EntityDataTable columns={columns} onRowClick={onOpen} />
+ <DataTablePagination table={table} />
+ </div>
+ </div>
+ </EntityTableContext.Provider>
+ );
+}
diff --git a/dashboard/src/features/entity-table/entity-table.tsx b/dashboard/src/features/entity-table/entity-table.tsx
index 0c9e260..1336f69 100644
--- a/dashboard/src/features/entity-table/entity-table.tsx
+++ b/dashboard/src/features/entity-table/entity-table.tsx
@@ -7,26 +7,26 @@ import { ColumnDef } from "@tanstack/react-table";
import { EntityTableContext } from "./contexts";
import { TableSearch, DataTablePagination, EntityDataTable } from "./components";
import {
- type UseRowSelectionReturn,
usePrimaryFiltering,
usePagination,
- type FetchEntityReturn,
useEntityTable,
useVisibility,
useSorting,
+ type UseRowSelectionReturn,
+ type FetchEntityReturn,
type QueryKey,
type EntityQueryKeyType,
useFilters,
} from "./hooks";
export interface EntityTableProps<T> {
- fetchEntity: ({ queryKey }: EntityQueryKeyType) => FetchEntityReturn<T>;
columns: ColumnDef<T>[];
primaryFilter: string;
entityKey: string;
rowSelection?: UseRowSelectionReturn;
- onCreate: () => void;
- onOpen: (entity: any) => void;
+ fetchEntity: ({ queryKey }: EntityQueryKeyType) => FetchEntityReturn<T>;
+ onCreate?: () => void;
+ onOpen?: (entity: any) => void;
}
export function EntityTable<T>({
@@ -44,6 +44,7 @@ export function EntityTable<T>({
const sorting = useSorting();
const visibility = useVisibility();
const { onPaginationChange, pageIndex, pageSize } = usePagination();
+
const query: QueryKey = [
entityKey,
{
@@ -61,7 +62,7 @@ export function EntityTable<T>({
const { data, isFetching } = useQuery({
queryFn: fetchEntity,
queryKey: query,
- initialData: { entity: [], pageCount: 1 },
+ initialData: { entities: [], pageCount: 1 },
});
const table = useEntityTable({
@@ -76,8 +77,8 @@ export function EntityTable<T>({
});
const contextValue = useMemo(
- () => ({ table, data: data.entity, primaryFilter: columnPrimaryFilter, filters, isLoading: isFetching }),
- [table, data.entity, filters, columnPrimaryFilter, isFetching],
+ () => ({ table, data: data.entities, primaryFilter: columnPrimaryFilter, filters, isLoading: isFetching }),
+ [table, data.entities, filters, columnPrimaryFilter, isFetching],
);
return (
diff --git a/dashboard/src/features/entity-table/hooks/use-entity-query.ts b/dashboard/src/features/entity-table/hooks/use-entity-query.ts
index 19bc0fc..5448e05 100644
--- a/dashboard/src/features/entity-table/hooks/use-entity-query.ts
+++ b/dashboard/src/features/entity-table/hooks/use-entity-query.ts
@@ -1,5 +1,5 @@
-interface PaginatedEntityQueryProps {
+export interface PaginatedEntityQueryProps {
page: number;
size: number;
}
@@ -20,11 +20,17 @@ interface FiltersEntityQueryProps {
export type UseEntityQueryProps = Partial<FiltersEntityQueryProps> & PaginatedEntityQueryProps & Partial<SortedEntityQueryProps>
export type EntityName = string;
+export type ParentEntityName = string;
export type EntityId = number | string | undefined;
export type PrimaryFilter = string;
export type QueryKey =
[EntityName, PaginatedEntityQueryProps, PrimaryFilter, SortedEntityQueryProps, FiltersEntityQueryProps]
+export type DoubleEntityQueryKey =
+ [EntityName, EntityId, PaginatedEntityQueryProps, PrimaryFilter?, SortedEntityQueryProps?, FiltersEntityQueryProps?]
+
+export type SelectableQueryKey =
+ [ParentEntityName, EntityId, EntityName, PaginatedEntityQueryProps, PrimaryFilter, SortedEntityQueryProps, FiltersEntityQueryProps]
export type SidebarQueryKey =
[EntityName, EntityId, EntityName, PaginatedEntityQueryProps, PrimaryFilter, SortedEntityQueryProps, FiltersEntityQueryProps]
@@ -34,11 +40,13 @@ export interface QueryKeyType<QT> {
}
export type EntityQueryKeyType = QueryKeyType<QueryKey>
+export type DoubleEntityQueryKeyType = QueryKeyType<DoubleEntityQueryKey>
+export type SelectableEntityQueryKeyType = QueryKeyType<SelectableQueryKey>
export type EntitySidebarQueryKeyType = QueryKeyType<SidebarQueryKey>
interface FetchEntityResult<T> {
pageCount: number
- entity: T[]
+ entities: T[]
}
export type FetchEntityReturn<T> = Promise<FetchEntityResult<T>>
diff --git a/dashboard/src/features/entity-table/hooks/use-entity-table.ts b/dashboard/src/features/entity-table/hooks/use-entity-table.ts
index 148866a..6103974 100644
--- a/dashboard/src/features/entity-table/hooks/use-entity-table.ts
+++ b/dashboard/src/features/entity-table/hooks/use-entity-table.ts
@@ -18,7 +18,7 @@ import type {
interface UseEntityTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: {
- entity: TData[];
+ entities: TData[];
pageCount: number;
};
sorting: UseSortingReturn;
@@ -42,7 +42,7 @@ export const useEntityTable = <TData, TValue>({
onPaginationChange,
}: UseEntityTableProps<TData, TValue>) =>
useReactTable({
- data: data.entity,
+ data: data.entities,
columns,
manualPagination: true,
manualSorting,
diff --git a/dashboard/src/features/entity-table/hooks/use-row-selection.ts b/dashboard/src/features/entity-table/hooks/use-row-selection.ts
index 106f763..153305b 100644
--- a/dashboard/src/features/entity-table/hooks/use-row-selection.ts
+++ b/dashboard/src/features/entity-table/hooks/use-row-selection.ts
@@ -1,6 +1,13 @@
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table"
+import { useState } from "react";
export interface UseRowSelectionReturn {
setSelectedRow: OnChangeFn<RowSelectionState>
selectedRow: RowSelectionState
}
+
+export const useRowSelection = (state: RowSelectionState): UseRowSelectionReturn => {
+ const [selectedRow, setSelectedRow] = useState<RowSelectionState>(state)
+ return { setSelectedRow, selectedRow }
+}
+
diff --git a/dashboard/src/features/entity-table/hooks/use-sidebar-entity-table.ts b/dashboard/src/features/entity-table/hooks/use-sidebar-entity-table.ts
index 8f429fb..4f9e3c7 100644
--- a/dashboard/src/features/entity-table/hooks/use-sidebar-entity-table.ts
+++ b/dashboard/src/features/entity-table/hooks/use-sidebar-entity-table.ts
@@ -79,7 +79,7 @@ export const useSidebarEntityTable = <T, S>({
const { data, isFetching } = useQuery({
queryFn: fetchEntity,
queryKey: query,
- initialData: { entity: [], pageCount: 1 },
+ initialData: { entities: [], pageCount: 1 },
});
const columns = columnsFn({ onEdit, onDelete, onOpen });
@@ -95,8 +95,8 @@ export const useSidebarEntityTable = <T, S>({
});
const entityTableContextValue = useMemo(
- () => ({ table, data: data.entity, primaryFilter, filters, isLoading: isFetching }),
- [table, data.entity, filters, primaryFilter, isFetching],
+ () => ({ table, data: data.entities, primaryFilter, filters, isLoading: isFetching }),
+ [table, data.entities, filters, primaryFilter, isFetching],
);
const sidebarEntityTableContextValue = useMemo(
diff --git a/dashboard/src/features/entity-table/index.ts b/dashboard/src/features/entity-table/index.ts
index 945c520..eb0cdb7 100644
--- a/dashboard/src/features/entity-table/index.ts
+++ b/dashboard/src/features/entity-table/index.ts
@@ -1,5 +1,7 @@
export * from "./entity-table";
+export * from "./selectable-entity-table";
export * from "./components";
export * from "./sidebar-entity-table";
+export * from "./double-entity-table";
export * from "./hooks";
export * from "./types";
diff --git a/dashboard/src/features/entity-table/selectable-entity-table.tsx b/dashboard/src/features/entity-table/selectable-entity-table.tsx
new file mode 100644
index 0000000..af1a5dc
--- /dev/null
+++ b/dashboard/src/features/entity-table/selectable-entity-table.tsx
@@ -0,0 +1,133 @@
+import { useEffect, useMemo } from "react";
+import { DataTableViewOptions } from "./components";
+import { useQuery } from "@tanstack/react-query";
+import { RowSelectionState, ColumnDef } from "@tanstack/react-table";
+import { EntityTableContext } from "./contexts";
+import { TableSearch, DataTablePagination, EntityDataTable } from "./components";
+import {
+ type UseRowSelectionReturn,
+ usePrimaryFiltering,
+ usePagination,
+ type FetchEntityReturn,
+ useEntityTable,
+ useVisibility,
+ useSorting,
+ type SelectableQueryKey,
+ type SelectableEntityQueryKeyType,
+ useFilters,
+} from "./hooks";
+
+export interface SelectableEntityTableProps<T extends { id: number }> {
+ fetchEntity: ({ queryKey }: SelectableEntityQueryKeyType) => FetchEntityReturn<T>;
+ columns: ColumnDef<T>[];
+ primaryFilter: string;
+ parentEntityKey: string;
+ parentEntityId: string | number;
+ entityKey: string;
+ rowSelection: UseRowSelectionReturn;
+ entitySelection: {
+ selectedEntity: number[];
+ setSelectedEntity: (s: number[]) => void;
+ }
+ existingEntityIds: number[];
+}
+
+export function SelectableEntityTable<T extends { id: number }>({
+ fetchEntity,
+ columns,
+ primaryFilter,
+ rowSelection,
+ entitySelection,
+ entityKey,
+ parentEntityKey,
+ parentEntityId,
+ existingEntityIds,
+}: SelectableEntityTableProps<T>) {
+ const columnPrimaryFilter = usePrimaryFiltering({ column: primaryFilter });
+ const filters = useFilters();
+ const sorting = useSorting();
+ const visibility = useVisibility();
+ const { selectedRow, setSelectedRow } = rowSelection;
+ const { setSelectedEntity } = entitySelection;
+ const { onPaginationChange, pageIndex, pageSize } = usePagination();
+ const query: SelectableQueryKey = [
+ parentEntityKey,
+ parentEntityId,
+ entityKey,
+ {
+ page: pageIndex,
+ size: pageSize,
+ },
+ columnPrimaryFilter.columnFilters,
+ {
+ sortBy: sorting.sorting[0]?.id || "created_at",
+ desc: sorting.sorting[0]?.desc || true,
+ },
+ { filters: filters.columnsFilter }
+ ];
+
+ const { data, isFetching } = useQuery({
+ queryFn: fetchEntity,
+ queryKey: query,
+ initialData: { entities: [], pageCount: 1 },
+ });
+
+ const table = useEntityTable({
+ data,
+ columns,
+ pageSize,
+ pageIndex,
+ rowSelection,
+ visibility,
+ sorting,
+ onPaginationChange,
+ });
+
+ useEffect(() => {
+ setSelectedRow((selected) => {
+ const updatedSelected: RowSelectionState = { ...selected };
+ for (const [i, fetchedEntity] of data.entities.entries()) {
+ if (existingEntityIds.includes(fetchedEntity.id)) {
+ updatedSelected[i] = true;
+ } else {
+ updatedSelected[i] = false;
+ }
+ }
+ return updatedSelected;
+ });
+
+ }, [data, setSelectedRow, existingEntityIds]);
+
+ useEffect(() => {
+ const selectedRowId = Object.keys(selectedRow).filter(key => selectedRow[key] === true).map(Number);
+
+ setSelectedEntity(
+ selectedRowId
+ .map(id => data.entities[id]?.id)
+ .filter(id => id !== null && id !== undefined)
+ );
+ }, [data, setSelectedEntity, setSelectedRow, existingEntityIds, selectedRow]);
+
+
+ const contextValue = useMemo(
+ () => ({ table, data: data.entities, primaryFilter: columnPrimaryFilter, filters, isLoading: isFetching }),
+ [table, data.entities, filters, columnPrimaryFilter, isFetching],
+ );
+
+ return (
+ <EntityTableContext.Provider value={contextValue}>
+ <div className="flex flex-col">
+ <div className="flex flex-col md:flex-row-reverse items-center py-4 gap-2 w-full">
+ <div className="flex flex-row items-center w-full">
+ <DataTableViewOptions table={table} />
+ </div>
+ <TableSearch />
+ </div>
+ <div className="w-full rounded-md border">
+ <EntityDataTable columns={columns} />
+ <DataTablePagination table={table} />
+ </div>
+ </div>
+ </EntityTableContext.Provider>
+ );
+}
diff --git a/dashboard/src/features/entity-table/stories/fetch-entities.ts b/dashboard/src/features/entity-table/stories/fetch-entities.ts
index 953ce97..719e0b7 100644
--- a/dashboard/src/features/entity-table/stories/fetch-entities.ts
+++ b/dashboard/src/features/entity-table/stories/fetch-entities.ts
@@ -64,7 +64,7 @@ export const fetchEntity = async ({ queryKey }: { queryKey: any }) => {
const paginatedEntities = filteredEntities.slice(startIndex, endIndex);
return {
- entity: paginatedEntities,
+ entities: paginatedEntities,
pageCount: Math.ceil(filteredEntities.length / pageSize),
};
}
@@ -73,7 +73,7 @@ export const fetchEntityLoading = async (): FetchEntityReturn<Entity> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
- entity: [{ id: 40, name: "Diyar Latif", createdAt: "2026-04-13" }],
+ entities: [{ id: 40, name: "Diyar Latif", createdAt: "2026-04-13" }],
pageCount: 1,
});
}, 24 * 60 * 60 * 1000);
diff --git a/dashboard/src/features/hosts/services/hosts.query.ts b/dashboard/src/features/hosts/services/hosts.query.ts
index 07c3008..6683bbb 100644
--- a/dashboard/src/features/hosts/services/hosts.query.ts
+++ b/dashboard/src/features/hosts/services/hosts.query.ts
@@ -21,7 +21,7 @@ export async function fetchHosts({ queryKey }: EntitySidebarQueryKeyType): Fetch
}
}).then((result) => {
return {
- entity: result.items,
+ entities: result.items,
pageCount: result.pages
};
});
@@ -35,6 +35,6 @@ export const useHostsQuery = ({
return useQuery({
queryKey: ["inbounds", inboundId, HostsQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchHosts,
- initialData: { entity: [], pageCount: 0 }
+ initialData: { entities: [], pageCount: 0 }
})
}
diff --git a/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx b/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx
index b420f55..a3d1486 100644
--- a/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx
+++ b/dashboard/src/features/hosts/tables/inbound-hosts/index.tsx
@@ -19,8 +19,8 @@ import {
} from "./inbound-sidebar-card";
export const InboundHostsTable = () => {
- const { data: inbounds } = useInboundsQuery()
- const [selectedInbound, setSelectedInbound] = useState<string | undefined>(inbounds[0]?.id !== undefined ? String(inbounds[0].id) : undefined)
+ const { data } = useInboundsQuery({ page: 1, size: 100 })
+ const [selectedInbound, setSelectedInbound] = useState<string | undefined>(data.entities[0]?.id !== undefined ? String(data.entities[0].id) : undefined)
const navigate = useNavigate({ from: "/hosts" })
const [inboundSelectionAlert, setInboundSelectionAlert] = useDialog();
@@ -51,7 +51,7 @@ export const InboundHostsTable = () => {
fetchEntity={fetchHosts}
entityKey="inbounds"
secondaryEntityKey="hosts"
- sidebarEntities={inbounds}
+ sidebarEntities={data.entities}
sidebarEntityId={selectedInbound}
columnsFn={columns}
filteredColumn='remark'
diff --git a/dashboard/src/features/inbounds/services/inbounds.query.ts b/dashboard/src/features/inbounds/services/inbounds.query.ts
index 6cb6ffc..22db81b 100644
--- a/dashboard/src/features/inbounds/services/inbounds.query.ts
+++ b/dashboard/src/features/inbounds/services/inbounds.query.ts
@@ -1,20 +1,41 @@
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
+import {
+ FetchEntityReturn,
+ UseEntityQueryProps,
+ EntityQueryKeyType
+} from "@marzneshin/features/entity-table";
import { InboundType } from "../types";
-export async function fetchInbounds(): Promise<InboundType[]> {
- return fetch('/inbounds').then((result) => {
- return result.items
- });
+export async function fetchInbounds({ queryKey }: EntityQueryKeyType): FetchEntityReturn<InboundType> {
+ const pagination = queryKey[1];
+ const primaryFilter = queryKey[2];
+ const filters = queryKey[4].filters;
+ return fetch(`/inbounds`, {
+ query: {
+ ...pagination,
+ ...filters,
+ tag: primaryFilter,
+ descending: queryKey[3].desc,
+ order_by: queryKey[3].sortBy,
+ }
+ }).then((result) => {
+ return {
+ entities: result.items,
+ pageCount: result.pages
+ };
+ })
}
export const InboundsQueryFetchKey = "inbounds";
-export const useInboundsQuery = () => {
+export const useInboundsQuery = ({
+ page, size, sortBy = "created_at", desc = false, filters = {}
+}: UseEntityQueryProps) => {
return useQuery({
- queryKey: [InboundsQueryFetchKey],
+ queryKey: [InboundsQueryFetchKey, { page, size }, filters?.tag ?? "", { sortBy, desc }, { filters }],
queryFn: fetchInbounds,
- initialData: []
+ initialData: { entities: [], pageCount: 0 }
})
}
diff --git a/dashboard/src/features/nodes/services/nodes.query.ts b/dashboard/src/features/nodes/services/nodes.query.ts
index 6602fbd..ae6a8c7 100644
--- a/dashboard/src/features/nodes/services/nodes.query.ts
+++ b/dashboard/src/features/nodes/services/nodes.query.ts
@@ -4,15 +4,16 @@ import { fetch } from "@marzneshin/utils";
import type {
EntityQueryKeyType,
UseEntityQueryProps,
+ FetchEntityReturn
} from "@marzneshin/features/entity-table";
export async function fetchNodes({
queryKey,
-}: EntityQueryKeyType): Promise<{ entity: NodeType[]; pageCount: number }> {
+}: EntityQueryKeyType): FetchEntityReturn<NodeType> {
const pagination = queryKey[1];
const primaryFilter = queryKey[2];
const filters = queryKey[4].filters;
- return fetch(`/nodes`, {
+ return fetch('/nodes', {
query: {
...pagination,
...filters,
@@ -22,7 +23,7 @@ export async function fetchNodes({
}
}).then((result) => {
return {
- entity: result.items,
+ entities: result.items,
pageCount: result.pages,
};
});
@@ -36,6 +37,6 @@ export const useNodesQuery = ({
return useQuery({
queryKey: [NodesQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchNodes,
- initialData: { entity: [], pageCount: 0 },
+ initialData: { entities: [], pageCount: 0 },
});
};
diff --git a/dashboard/src/features/services/components/dialogs/mutation/fields/inbounds.tsx b/dashboard/src/features/services/components/dialogs/mutation/fields/inbounds.tsx
index b13e192..def8cd1 100644
--- a/dashboard/src/features/services/components/dialogs/mutation/fields/inbounds.tsx
+++ b/dashboard/src/features/services/components/dialogs/mutation/fields/inbounds.tsx
@@ -61,7 +61,7 @@ export const InboundCard = ({
};
export const InboundsField = () => {
- const { data: inbounds } = useInboundsQuery();
+ const { data } = useInboundsQuery({ page: 1, size: 100 });
const form = useFormContext();
const { t } = useTranslation();
@@ -73,7 +73,7 @@ export const InboundsField = () => {
<FormItem>
<FormLabel>{t("inbounds")}</FormLabel>
<ScrollArea className="flex flex-col justify-start h-full max-h-[20rem]">
- {inbounds.map((inbound) => (
+ {data.entities.map((inbound) => (
<FormField
key={inbound.id}
control={form.control}
diff --git a/dashboard/src/features/services/components/form-fields/services.tsx b/dashboard/src/features/services/components/form-fields/services.tsx
index 45e73c9..51b8b95 100644
--- a/dashboard/src/features/services/components/form-fields/services.tsx
+++ b/dashboard/src/features/services/components/form-fields/services.tsx
@@ -123,9 +123,9 @@ export const ServicesField: FC = () => {
<Awaiting
isFetching={isFetching}
Skeleton={<ServiceSkeletons />}
- isNotFound={data.entity.length === 0}
+ isNotFound={data.entities.length === 0}
NotFound={<ServicesAlert />}
- Component={<ServicesList services={data.entity} />}
+ Component={<ServicesList services={data.entities} />}
/>
</FormItem>
)}
diff --git a/dashboard/src/features/services/components/tables/inbounds/index.tsx b/dashboard/src/features/services/components/tables/inbounds/index.tsx
index 1e8aeec..613b57d 100644
--- a/dashboard/src/features/services/components/tables/inbounds/index.tsx
+++ b/dashboard/src/features/services/components/tables/inbounds/index.tsx
@@ -1,11 +1,9 @@
-import { type FC, useCallback, useEffect, useState } from "react";
-import { useInboundsQuery } from "@marzneshin/features/inbounds";
+import { type FC, useCallback, useState } from "react";
import { Button } from "@marzneshin/components";
-import { DataTable } from "@marzneshin/features/entity-table";
+import { SelectableEntityTable, useRowSelection } from "@marzneshin/features/entity-table";
import { columns } from "./columns";
-import { type ServiceType, useServicesUpdateMutation } from "@marzneshin/features/services";
+import { type ServiceType, useServicesUpdateMutation, fetchSelectableServiceInbounds } from "@marzneshin/features/services";
import { useTranslation } from "react-i18next";
-import type { RowSelectionState } from "@tanstack/react-table";
interface ServiceInboundsTableProps {
service: ServiceType;
@@ -15,43 +13,30 @@ export const ServiceInboundsTable: FC<ServiceInboundsTableProps> = ({
service,
}) => {
const { mutate: updateService } = useServicesUpdateMutation();
- const { data } = useInboundsQuery();
- const [selectedInbound, setSelectedInbound] = useState<{
- [key: number]: boolean;
- }>({});
+ const { selectedRow, setSelectedRow } =
+ useRowSelection(Object.fromEntries(service.inbound_ids.map(entityId => [String(entityId), true])));
+ const [selectedInbound, setSelectedInbound] = useState<number[]>(service.inbound_ids);
const { t } = useTranslation();
- useEffect(() => {
- setSelectedInbound((prevSelected) => {
- const updatedSelected: RowSelectionState = { ...prevSelected };
- for (const inboundId of service.inbound_ids) {
- for (const [i, fetchedInbound] of data.entries()) {
- if (fetchedInbound.id === inboundId) {
- updatedSelected[i] = true;
- }
- }
- }
- return updatedSelected;
- });
- }, [data, service.inbound_ids]);
-
const handleApply = useCallback(() => {
- const selectedInboundIds = Object.keys(selectedInbound)
- .filter((key) => selectedInbound[Number.parseInt(key)])
- .map((key) => data[Number.parseInt(key)].id);
- updateService({ ...service, inbound_ids: selectedInboundIds });
- }, [data, selectedInbound, service, updateService]);
+ console.log(selectedInbound)
+ updateService({ id: service.id, name: service.name, inbound_ids: selectedInbound });
+ }, [selectedInbound, service, updateService]);
- const disabled = Object.keys(selectedInbound).length < 1;
+ const disabled = Object.keys(selectedRow).length < 1;
return (
<div className="flex flex-col gap-4">
- <DataTable
+ <SelectableEntityTable
columns={columns}
- data={data}
- filteredColumn="tag"
- selectedRow={selectedInbound}
- setSelectedRow={setSelectedInbound}
+ entityKey="inbounds"
+ parentEntityKey="services"
+ parentEntityId={service.id}
+ existingEntityIds={service.inbound_ids}
+ fetchEntity={fetchSelectableServiceInbounds}
+ primaryFilter="tag"
+ rowSelection={{ selectedRow: selectedRow, setSelectedRow: setSelectedRow }}
+ entitySelection={{ selectedEntity: selectedInbound, setSelectedEntity: setSelectedInbound }}
/>
<Button onClick={handleApply} disabled={disabled}>
diff --git a/dashboard/src/features/services/components/tables/users/columns.tsx b/dashboard/src/features/services/components/tables/users/columns.tsx
index a404ac7..eec363d 100644
--- a/dashboard/src/features/services/components/tables/users/columns.tsx
+++ b/dashboard/src/features/services/components/tables/users/columns.tsx
@@ -4,12 +4,10 @@ import i18n from "@marzneshin/features/i18n"
import {
UserType,
UserActivatedPill,
- UserEnabledPill,
+ UserUsedTraffic,
UserExpirationValue,
userTrafficSortingFn,
} from "@marzneshin/features/users"
-import { CircularProgress } from "@nextui-org/progress";
-import { Badge } from "@marzneshin/components";
export const columns: ColumnDef<UserType>[] = [
{
@@ -27,30 +25,14 @@ export const columns: ColumnDef<UserType>[] = [
cell: ({ row }) => <UserActivatedPill user={row.original} />,
},
{
- accessorKey: "enabled",
+ accessorKey: "used_traffic",
header: ({ column }) => (
<DataTableColumnHeader
- title={i18n.t("enabled")}
+ title={i18n.t("page.users.used_traffic")}
column={column}
/>
),
- cell: ({ row }) => <UserEnabledPill user={row.original} />,
- },
- {
- accessorKey: "used_traffic",
- header: ({ column }) => <DataTableColumnHeader title={i18n.t('page.users.used_traffic')} column={column} />,
- cell: ({ row }) => {
- if (row.original.data_limit) {
- return (
- <CircularProgress
- size="sm"
- value={row.original.used_traffic / row.original.data_limit * 100}
- />
- )
- } else {
- return <Badge>No Limit</Badge>
- }
- },
+ cell: ({ row }) => <UserUsedTraffic user={row.original} />,
sortingFn: (rowA, rowB) => userTrafficSortingFn(rowA.original, rowB.original)
},
{
diff --git a/dashboard/src/features/services/components/tables/users/index.tsx b/dashboard/src/features/services/components/tables/users/index.tsx
index 486e2f5..324bdad 100644
--- a/dashboard/src/features/services/components/tables/users/index.tsx
+++ b/dashboard/src/features/services/components/tables/users/index.tsx
@@ -1,21 +1,22 @@
import { FC } from "react";
-import { DataTable } from "@marzneshin/features/entity-table";
+import { DoubleEntityTable } from "@marzneshin/features/entity-table";
import { columns } from "./columns";
-import { useUsersServiceQuery, type ServiceType } from "@marzneshin/features/services";
+import { fetchServiceUsers, type ServiceType } from "@marzneshin/features/services";
interface ServicesUsersTableProps {
service: ServiceType
}
export const ServicesUsersTable: FC<ServicesUsersTableProps> = ({ service }) => {
- const { data } = useUsersServiceQuery({ serviceId: service.id });
return (
- <DataTable
+ <DoubleEntityTable
columns={columns}
- data={data}
- filteredColumn='username'
+ entityId={service.id}
+ fetchEntity={fetchServiceUsers}
+ primaryFilter="username"
+ entityKey='services'
/>
)
}
diff --git a/dashboard/src/features/services/services/service-inbounds.query.ts b/dashboard/src/features/services/services/service-inbounds.query.ts
index 2d6419b..2409f01 100644
--- a/dashboard/src/features/services/services/service-inbounds.query.ts
+++ b/dashboard/src/features/services/services/service-inbounds.query.ts
@@ -1,6 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
import { InboundType } from "@marzneshin/features/inbounds";
+import {
+ FetchEntityReturn,
+ SelectableEntityQueryKeyType
+} from "@marzneshin/features/entity-table";
interface FetchServiceInboundsType {
queryKey: [string, number, number, number]
@@ -25,6 +29,27 @@ export async function fetchServiceInbounds({
});
}
+
+export async function fetchSelectableServiceInbounds({ queryKey }: SelectableEntityQueryKeyType): FetchEntityReturn<InboundType> {
+ const pagination = queryKey[3];
+ const primaryFilter = queryKey[4];
+ const filters = queryKey[6].filters;
+ return fetch(`/inbounds`, {
+ query: {
+ ...pagination,
+ ...filters,
+ tag: primaryFilter,
+ descending: queryKey[5].desc,
+ order_by: queryKey[5].sortBy,
+ }
+ }).then((result) => {
+ return {
+ entities: result.items,
+ pageCount: result.pages
+ };
+ })
+}
+
const ServicesQueryFetchKey = "services";
export const useInboundsServiceQuery = ({
diff --git a/dashboard/src/features/services/services/service-users.query.ts b/dashboard/src/features/services/services/service-users.query.ts
index 201ae98..e601fd4 100644
--- a/dashboard/src/features/services/services/service-users.query.ts
+++ b/dashboard/src/features/services/services/service-users.query.ts
@@ -1,25 +1,29 @@
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
import { UserType } from "@marzneshin/features/users";
+import type {
+ DoubleEntityQueryKeyType,
+ UseEntityQueryProps,
+ FetchEntityReturn
+} from "@marzneshin/features/entity-table";
-interface FetchServiceUsersType {
- queryKey: [string, number, number, number]
-}
-
-interface UseServiceUsersQueryProps {
+interface UseServiceUsersQueryProps extends UseEntityQueryProps {
serviceId: number;
- page?: number;
- size?: number;
}
-export async function fetchServiceUsers({ queryKey }: FetchServiceUsersType): Promise<UserType[]> {
+export async function fetchServiceUsers({ queryKey }: DoubleEntityQueryKeyType): FetchEntityReturn<UserType> {
+ const pagination = queryKey[2];
+ const primaryFilter = queryKey[3];
return fetch(`/services/${queryKey[1]}/users`, {
query: {
- page: queryKey[2],
- size: queryKey[3]
+ ...pagination,
+ username: primaryFilter,
}
}).then((result) => {
- return result.items;
+ return {
+ entities: result.items,
+ pageCount: result.pages,
+ };
});
}
@@ -29,8 +33,8 @@ export const useUsersServiceQuery = ({
serviceId, page = 1, size = 50
}: UseServiceUsersQueryProps) => {
return useQuery({
- queryKey: [ServicesQueryFetchKey, serviceId, page, size],
+ queryKey: [ServicesQueryFetchKey, serviceId, { page, size }],
queryFn: fetchServiceUsers,
- initialData: []
+ initialData: { entities: [], pageCount: 0 },
})
}
diff --git a/dashboard/src/features/services/services/services.query.ts b/dashboard/src/features/services/services/services.query.ts
index cb16de2..82cb479 100644
--- a/dashboard/src/features/services/services/services.query.ts
+++ b/dashboard/src/features/services/services/services.query.ts
@@ -2,6 +2,7 @@ import { fetch } from "@marzneshin/utils";
import { ServiceType } from "../types";
import {
EntityQueryKeyType,
+ SelectableEntityQueryKeyType,
FetchEntityReturn,
UseEntityQueryProps
} from "@marzneshin/features/entity-table";
@@ -22,12 +23,34 @@ export async function fetchServices({ queryKey }: EntityQueryKeyType): FetchEnti
}).then((result) => {
const services: ServiceType[] = result.items;
return {
- entity: services,
+ entities: services,
pageCount: result.pages
};
});
}
+export async function fetchUserServices({ queryKey }: SelectableEntityQueryKeyType): FetchEntityReturn<ServiceType> {
+ const pagination = queryKey[3];
+ const primaryFilter = queryKey[4];
+ const filters = queryKey[6].filters;
+ return fetch(`/services`, {
+ query: {
+ ...pagination,
+ ...filters,
+ name: primaryFilter,
+ descending: queryKey[5].desc,
+ order_by: queryKey[5].sortBy,
+ }
+ }).then((result) => {
+ const services: ServiceType[] = result.items;
+ return {
+ entities: services,
+ pageCount: result.pages
+ };
+ });
+}
+
+
export const ServicesQueryFetchKey = "services";
export const useServicesQuery = ({
@@ -36,6 +59,6 @@ export const useServicesQuery = ({
return useQuery({
queryKey: [ServicesQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchServices,
- initialData: { entity: [], pageCount: 0 }
+ initialData: { entities: [], pageCount: 0 }
})
}
diff --git a/dashboard/src/features/services/services/update-service.mutate.ts b/dashboard/src/features/services/services/update-service.mutate.ts
index f788e4c..89beb66 100644
--- a/dashboard/src/features/services/services/update-service.mutate.ts
+++ b/dashboard/src/features/services/services/update-service.mutate.ts
@@ -6,8 +6,8 @@ import { ServicesQueryFetchKey } from "./services.query";
import { ServiceMutationType } from "../types";
-export async function updateService(node: ServiceMutationType): Promise<ServiceMutationType> {
- return fetch(`/services/${node.id}`, { method: 'put', body: node }).then((service) => {
+export async function updateService(service: ServiceMutationType): Promise<ServiceMutationType> {
+ return fetch(`/services/${service.id}`, { method: 'put', body: service }).then((service) => {
return service;
});
}
diff --git a/dashboard/src/features/users/components/tables/services/index.tsx b/dashboard/src/features/users/components/tables/services/index.tsx
index b2326bd..71f82c5 100644
--- a/dashboard/src/features/users/components/tables/services/index.tsx
+++ b/dashboard/src/features/users/components/tables/services/index.tsx
@@ -1,62 +1,48 @@
-import { type FC, useCallback, useEffect, useState } from "react";
+import { type FC, useCallback, useState } from "react";
import { Button } from "@marzneshin/components";
-import { DataTable } from "@marzneshin/features/entity-table";
+import { SelectableEntityTable, useRowSelection } from "@marzneshin/features/entity-table";
import { columns } from "./columns";
import {
type UserType,
useUsersUpdateMutation,
} from "@marzneshin/features/users";
import { useTranslation } from "react-i18next";
-import type { RowSelectionState } from "@tanstack/react-table";
-import { useServicesQuery } from "@marzneshin/features/services";
+import { fetchUserServices } from "@marzneshin/features/services";
interface UserServicesTableProps {
user: UserType;
}
-// TODO: #322 Implement pagination for row selectable with appliedable action table
export const UserServicesTable: FC<UserServicesTableProps> = ({ user }) => {
const { mutate: updateUser } = useUsersUpdateMutation();
- const { data } = useServicesQuery({ page: 1, size: 100 });
- const [selectedService, setSelectedService] = useState<{
- [key: number]: boolean;
- }>({});
+ const { selectedRow, setSelectedRow } =
+ useRowSelection(
+ Object.fromEntries(
+ user.service_ids.map(entityId => [String(entityId), true])
+ )
+ );
+ const [selectedService, setSelectedService] = useState<number[]>(user.service_ids);
const { t } = useTranslation();
- useEffect(() => {
- setSelectedService((prevSelected) => {
- const updatedSelected: RowSelectionState = { ...prevSelected };
- for (const serviceId of user.service_ids) {
- for (const [i, fetchedService] of data.entity.entries()) {
- if (fetchedService.id === serviceId) {
- updatedSelected[i] = true;
- }
- }
- }
- return updatedSelected;
- });
- }, [data, user.service_ids]);
-
const handleApply = useCallback(() => {
- const selectedServiceIds = Object.keys(selectedService)
- .filter((key) => selectedService[Number.parseInt(key)])
- .map((key) => data.entity[Number.parseInt(key)].id);
- updateUser({ ...user, service_ids: selectedServiceIds });
- }, [data, selectedService, user, updateUser]);
-
- const disabled = Object.keys(selectedService).length < 1;
+ updateUser({ ...user, service_ids: selectedService });
+ }, [selectedService, user, updateUser]);
return (
<div className="flex flex-col gap-4">
- <DataTable
+ <SelectableEntityTable
+ fetchEntity={fetchUserServices}
columns={columns}
- data={data.entity}
- filteredColumn="name"
- selectedRow={selectedService}
- setSelectedRow={setSelectedService}
+ primaryFilter="name"
+ existingEntityIds={user.service_ids}
+ entityKey="services"
+ parentEntityKey="users"
+ parentEntityId={user.username}
+ rowSelection={{ selectedRow: selectedRow, setSelectedRow: setSelectedRow }}
+ entitySelection={{ selectedEntity: selectedService, setSelectedEntity: setSelectedService }}
/>
- <Button onClick={handleApply} disabled={disabled}>
+ <Button onClick={handleApply} disabled={selectedService.length === 0}>
{t("apply")}
</Button>
</div>
diff --git a/dashboard/src/features/users/components/tables/users/columns.tsx b/dashboard/src/features/users/components/tables/users/columns.tsx
index 2cb2bad..09db732 100644
--- a/dashboard/src/features/users/components/tables/users/columns.tsx
+++ b/dashboard/src/features/users/components/tables/users/columns.tsx
@@ -6,7 +6,7 @@ import {
UserExpireStrategyPill,
UserExpirationValue
} from "@marzneshin/features/users";
-import { useAdminsQuery } from "@marzneshin/features/admins";
+import { useAdminsQuery } from "@marzneshin/modules/admins";
import i18n from "@marzneshin/features/i18n";
import {
CopyToClipboardButton,
@@ -111,7 +111,7 @@ function AdminsColumnsHeaderOptionFilter<TData, TValue>({ column }: { column: Co
<DataTableColumnHeaderFilterOption
title={i18n.t("owner")}
column={column}
- options={data.entity.map((admin) => admin.username)}
+ options={data.entities.map((admin) => admin.username)}
/>
);
}
diff --git a/dashboard/src/features/users/services/user.query.ts b/dashboard/src/features/users/services/user.query.ts
index 1838d13..72da827 100644
--- a/dashboard/src/features/users/services/user.query.ts
+++ b/dashboard/src/features/users/services/user.query.ts
@@ -19,9 +19,5 @@ export const userQueryOptions = ({ username }: { username: string }) => {
};
export const useUserQuery = ({ username }: { username: string }) => {
- return useQuery({
- queryKey: [UserQueryFetchKey, username],
- queryFn: fetchUser,
- initialData: null,
- });
+ return useQuery(userQueryOptions({ username }));
};
diff --git a/dashboard/src/features/users/services/users.query.ts b/dashboard/src/features/users/services/users.query.ts
index 3c3a5d6..e05f298 100644
--- a/dashboard/src/features/users/services/users.query.ts
+++ b/dashboard/src/features/users/services/users.query.ts
@@ -24,7 +24,7 @@ export async function fetchUsers({ queryKey }: EntityQueryKeyType): FetchEntityR
}
}).then((result) => {
return {
- entity: result.items,
+ entities: result.items,
pageCount: result.pages
};
});
@@ -38,6 +38,6 @@ export const useUsersQuery = ({
return useQuery({
queryKey: [UsersQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchUsers,
- initialData: { entity: [], pageCount: 0 }
+ initialData: { entities: [], pageCount: 0 }
})
}
diff --git a/dashboard/src/features/admins/components/dialogs/delete-confirmation/index.tsx b/dashboard/src/modules/admins/components/dialogs/delete-confirmation/index.tsx
similarity index 95%
rename from dashboard/src/features/admins/components/dialogs/delete-confirmation/index.tsx
rename to dashboard/src/modules/admins/components/dialogs/delete-confirmation/index.tsx
index 1a0e4e2..c2c8b05 100644
--- a/dashboard/src/features/admins/components/dialogs/delete-confirmation/index.tsx
+++ b/dashboard/src/modules/admins/components/dialogs/delete-confirmation/index.tsx
@@ -3,7 +3,7 @@ import { type FC, useEffect } from "react";
import {
type AdminType,
useAdminsDeletionMutation,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
interface AdminsDeleteConfirmationDialogProps {
onOpenChange: (state: boolean) => void;
diff --git a/dashboard/src/features/admins/components/dialogs/index.ts b/dashboard/src/modules/admins/components/dialogs/index.ts
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/index.ts
rename to dashboard/src/modules/admins/components/dialogs/index.ts
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/all-services-access.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/fields/all-services-access.tsx
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/all-services-access.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/all-services-access.tsx
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/enabled.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/fields/enabled.tsx
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/enabled.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/enabled.tsx
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/index.ts b/dashboard/src/modules/admins/components/dialogs/mutation/fields/index.ts
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/index.ts
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/index.ts
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/modify-users-access.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/fields/modify-users-access.tsx
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/modify-users-access.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/modify-users-access.tsx
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/password.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/fields/password.tsx
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/password.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/password.tsx
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/subscription-url-prefix.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/fields/subscription-url-prefix.tsx
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/subscription-url-prefix.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/subscription-url-prefix.tsx
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/fields/sudo-privilage.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/fields/sudo-privilage.tsx
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/mutation/fields/sudo-privilage.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/fields/sudo-privilage.tsx
diff --git a/dashboard/src/features/admins/components/dialogs/mutation/index.tsx b/dashboard/src/modules/admins/components/dialogs/mutation/index.tsx
similarity index 98%
rename from dashboard/src/features/admins/components/dialogs/mutation/index.tsx
rename to dashboard/src/modules/admins/components/dialogs/mutation/index.tsx
index be4d56e..1b96af2 100644
--- a/dashboard/src/features/admins/components/dialogs/mutation/index.tsx
+++ b/dashboard/src/modules/admins/components/dialogs/mutation/index.tsx
@@ -16,7 +16,7 @@ import {
AdminEditSchema,
AdminCreateSchema,
AdminType,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
import { ServicesField } from "@marzneshin/features/services";
import { UsernameField } from "@marzneshin/features/users";
import {
diff --git a/dashboard/src/features/admins/components/dialogs/settings/admin-info/index.ts b/dashboard/src/modules/admins/components/dialogs/settings/admin-info/index.ts
similarity index 100%
rename from dashboard/src/features/admins/components/dialogs/settings/admin-info/index.ts
rename to dashboard/src/modules/admins/components/dialogs/settings/admin-info/index.ts
diff --git a/dashboard/src/features/admins/components/dialogs/settings/admin-info/table.tsx b/dashboard/src/modules/admins/components/dialogs/settings/admin-info/table.tsx
similarity index 97%
rename from dashboard/src/features/admins/components/dialogs/settings/admin-info/table.tsx
rename to dashboard/src/modules/admins/components/dialogs/settings/admin-info/table.tsx
index 10ed23d..02812f3 100644
--- a/dashboard/src/features/admins/components/dialogs/settings/admin-info/table.tsx
+++ b/dashboard/src/modules/admins/components/dialogs/settings/admin-info/table.tsx
@@ -12,7 +12,7 @@ import {
type AdminProp,
AdminEnabledPill,
AdminPermissionPill
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
import { useTranslation } from "react-i18next";
import { format } from '@chbphone55/pretty-bytes';
diff --git a/dashboard/src/features/admins/components/dialogs/settings/index.tsx b/dashboard/src/modules/admins/components/dialogs/settings/index.tsx
similarity index 96%
rename from dashboard/src/features/admins/components/dialogs/settings/index.tsx
rename to dashboard/src/modules/admins/components/dialogs/settings/index.tsx
index 60b31f8..8af5dd4 100644
--- a/dashboard/src/features/admins/components/dialogs/settings/index.tsx
+++ b/dashboard/src/modules/admins/components/dialogs/settings/index.tsx
@@ -1,5 +1,4 @@
import {
- // Button,
Tabs,
TabsContent,
TabsList,
@@ -9,7 +8,7 @@ import {
SettingsDialogProps,
SettingsDialog,
} from "@marzneshin/components";
-import { type AdminType } from "@marzneshin/features/admins";
+import { type AdminType } from "@marzneshin/modules/admins";
import { type FC } from "react";
import { AdminInfoTable } from "./admin-info";
import { useTranslation } from "react-i18next";
diff --git a/dashboard/src/features/admins/components/index.ts b/dashboard/src/modules/admins/components/index.ts
similarity index 100%
rename from dashboard/src/features/admins/components/index.ts
rename to dashboard/src/modules/admins/components/index.ts
diff --git a/dashboard/src/features/admins/components/pills/admin-enabled-pill.tsx b/dashboard/src/modules/admins/components/pills/admin-enabled-pill.tsx
similarity index 87%
rename from dashboard/src/features/admins/components/pills/admin-enabled-pill.tsx
rename to dashboard/src/modules/admins/components/pills/admin-enabled-pill.tsx
index 4ca3943..19d8dc5 100644
--- a/dashboard/src/features/admins/components/pills/admin-enabled-pill.tsx
+++ b/dashboard/src/modules/admins/components/pills/admin-enabled-pill.tsx
@@ -1,7 +1,7 @@
import { type FC } from "react";
import { BooleanPill } from "@marzneshin/components";
import { useTranslation } from "react-i18next";
-import { AdminProp } from "@marzneshin/features/admins";
+import { AdminProp } from "@marzneshin/modules/admins";
export const AdminEnabledPill: FC<AdminProp> = ({ admin }) => {
const { t } = useTranslation();
diff --git a/dashboard/src/features/admins/components/pills/admin-permission-pill.tsx b/dashboard/src/modules/admins/components/pills/admin-permission-pill.tsx
similarity index 89%
rename from dashboard/src/features/admins/components/pills/admin-permission-pill.tsx
rename to dashboard/src/modules/admins/components/pills/admin-permission-pill.tsx
index 5fbf539..ac1b340 100644
--- a/dashboard/src/features/admins/components/pills/admin-permission-pill.tsx
+++ b/dashboard/src/modules/admins/components/pills/admin-permission-pill.tsx
@@ -1,7 +1,7 @@
import { type FC } from "react";
import { BooleanPill } from "@marzneshin/components";
import { useTranslation } from "react-i18next";
-import { AdminProp } from "@marzneshin/features/admins";
+import { AdminProp } from "@marzneshin/modules/admins";
export const AdminPermissionPill: FC<AdminProp> = ({ admin }) => {
const { t } = useTranslation();
diff --git a/dashboard/src/features/admins/components/pills/index.ts b/dashboard/src/modules/admins/components/pills/index.ts
similarity index 100%
rename from dashboard/src/features/admins/components/pills/index.ts
rename to dashboard/src/modules/admins/components/pills/index.ts
diff --git a/dashboard/src/features/admins/components/tables/admins/columns.tsx b/dashboard/src/modules/admins/components/tables/admins/columns.tsx
similarity index 97%
rename from dashboard/src/features/admins/components/tables/admins/columns.tsx
rename to dashboard/src/modules/admins/components/tables/admins/columns.tsx
index b206d2f..993d9b9 100644
--- a/dashboard/src/features/admins/components/tables/admins/columns.tsx
+++ b/dashboard/src/modules/admins/components/tables/admins/columns.tsx
@@ -3,7 +3,7 @@ import {
type AdminType,
AdminEnabledPill,
AdminPermissionPill,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
import {
DataTableActionsCell,
DataTableColumnHeader
diff --git a/dashboard/src/features/admins/components/tables/admins/index.tsx b/dashboard/src/modules/admins/components/tables/admins/index.tsx
similarity index 97%
rename from dashboard/src/features/admins/components/tables/admins/index.tsx
rename to dashboard/src/modules/admins/components/tables/admins/index.tsx
index c1787d5..9c124d1 100644
--- a/dashboard/src/features/admins/components/tables/admins/index.tsx
+++ b/dashboard/src/modules/admins/components/tables/admins/index.tsx
@@ -1,6 +1,6 @@
import type { FC } from "react";
import { ColumnDef } from "@tanstack/react-table";
-import { fetchAdmins, AdminType, AdminsQueryFetchKey } from "@marzneshin/features/admins";
+import { fetchAdmins, AdminType, AdminsQueryFetchKey } from "@marzneshin/modules/admins";
import { columns as columnsFn } from "./columns";
import { EntityTable } from "@marzneshin/features/entity-table";
import { useNavigate } from "@tanstack/react-router";
diff --git a/dashboard/src/features/admins/components/tables/index.ts b/dashboard/src/modules/admins/components/tables/index.ts
similarity index 100%
rename from dashboard/src/features/admins/components/tables/index.ts
rename to dashboard/src/modules/admins/components/tables/index.ts
diff --git a/dashboard/src/features/admins/contexts/router.ts b/dashboard/src/modules/admins/contexts/router.ts
similarity index 85%
rename from dashboard/src/features/admins/contexts/router.ts
rename to dashboard/src/modules/admins/contexts/router.ts
index 41933a5..1198e98 100644
--- a/dashboard/src/features/admins/contexts/router.ts
+++ b/dashboard/src/modules/admins/contexts/router.ts
@@ -1,5 +1,5 @@
import { createContext, useContext } from "react";
-import { AdminType } from "@marzneshin/features/admins";
+import { AdminType } from "@marzneshin/modules/admins";
interface RouterAdminContextProps {
admin: AdminType | null;
diff --git a/dashboard/src/features/admins/index.ts b/dashboard/src/modules/admins/index.ts
similarity index 100%
rename from dashboard/src/features/admins/index.ts
rename to dashboard/src/modules/admins/index.ts
diff --git a/dashboard/src/features/admins/services/admin.query.ts b/dashboard/src/modules/admins/services/admin.query.ts
similarity index 100%
rename from dashboard/src/features/admins/services/admin.query.ts
rename to dashboard/src/modules/admins/services/admin.query.ts
diff --git a/dashboard/src/features/admins/services/admins.query.ts b/dashboard/src/modules/admins/services/admins.query.ts
similarity index 88%
rename from dashboard/src/features/admins/services/admins.query.ts
rename to dashboard/src/modules/admins/services/admins.query.ts
index 51d23c2..81c9c79 100644
--- a/dashboard/src/features/admins/services/admins.query.ts
+++ b/dashboard/src/modules/admins/services/admins.query.ts
@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@marzneshin/utils";
-import { AdminType } from "@marzneshin/features/admins";
+import { AdminType } from "@marzneshin/modules/admins";
import {
FetchEntityReturn,
UseEntityQueryProps,
@@ -21,7 +21,7 @@ export async function fetchAdmins({ queryKey }: EntityQueryKeyType): FetchEntity
}
}).then((result) => {
return {
- entity: result.items,
+ entities: result.items,
pageCount: result.pages
};
});
@@ -35,6 +35,6 @@ export const useAdminsQuery = ({
return useQuery({
queryKey: [AdminsQueryFetchKey, { page, size }, filters?.username ?? "", { sortBy, desc }, { filters }],
queryFn: fetchAdmins,
- initialData: { entity: [], pageCount: 0 }
+ initialData: { entities: [], pageCount: 0 }
})
}
diff --git a/dashboard/src/features/admins/services/create-admin.mutate.ts b/dashboard/src/modules/admins/services/create-admin.mutate.ts
similarity index 97%
rename from dashboard/src/features/admins/services/create-admin.mutate.ts
rename to dashboard/src/modules/admins/services/create-admin.mutate.ts
index 50e77ad..fed6527 100644
--- a/dashboard/src/features/admins/services/create-admin.mutate.ts
+++ b/dashboard/src/modules/admins/services/create-admin.mutate.ts
@@ -1,4 +1,4 @@
-import { AdminType, AdminMutationType, AdminsQueryFetchKey } from "@marzneshin/features/admins";
+import { AdminType, AdminMutationType, AdminsQueryFetchKey } from "@marzneshin/modules/admins";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/admins/services/delete-admin.mutate.ts b/dashboard/src/modules/admins/services/delete-admin.mutate.ts
similarity index 93%
rename from dashboard/src/features/admins/services/delete-admin.mutate.ts
rename to dashboard/src/modules/admins/services/delete-admin.mutate.ts
index 1d6c7e8..d00e7b3 100644
--- a/dashboard/src/features/admins/services/delete-admin.mutate.ts
+++ b/dashboard/src/modules/admins/services/delete-admin.mutate.ts
@@ -1,4 +1,4 @@
-import { AdminType, AdminsQueryFetchKey } from "@marzneshin/features/admins";
+import { AdminType, AdminsQueryFetchKey } from "@marzneshin/modules/admins";
import { useMutation } from "@tanstack/react-query";
import { fetch, queryClient } from "@marzneshin/utils";
import { toast } from "sonner";
diff --git a/dashboard/src/features/admins/services/index.ts b/dashboard/src/modules/admins/services/index.ts
similarity index 100%
rename from dashboard/src/features/admins/services/index.ts
rename to dashboard/src/modules/admins/services/index.ts
diff --git a/dashboard/src/features/admins/services/update-admin.mutate.ts b/dashboard/src/modules/admins/services/update-admin.mutate.ts
similarity index 97%
rename from dashboard/src/features/admins/services/update-admin.mutate.ts
rename to dashboard/src/modules/admins/services/update-admin.mutate.ts
index 5cac34a..e2ca624 100644
--- a/dashboard/src/features/admins/services/update-admin.mutate.ts
+++ b/dashboard/src/modules/admins/services/update-admin.mutate.ts
@@ -6,7 +6,7 @@ import {
AdminsQueryFetchKey,
AdminMutationType,
AdminType
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
export async function updateAdmin(admin: AdminMutationType): Promise<AdminType> {
return fetch(`/admins/${admin.username}`, { method: 'put', body: admin }).then((admin) => {
diff --git a/dashboard/src/modules/admins/types/admin-prop.ts b/dashboard/src/modules/admins/types/admin-prop.ts
new file mode 100644
index 0000000..e6c7f4e
--- /dev/null
+++ b/dashboard/src/modules/admins/types/admin-prop.ts
@@ -0,0 +1,5 @@
+import { AdminType } from "@marzneshin/modules/admins";
+
+export interface AdminProp {
+ admin: AdminType;
+}
diff --git a/dashboard/src/features/admins/types/admin.ts b/dashboard/src/modules/admins/types/admin.ts
similarity index 100%
rename from dashboard/src/features/admins/types/admin.ts
rename to dashboard/src/modules/admins/types/admin.ts
diff --git a/dashboard/src/features/admins/types/index.ts b/dashboard/src/modules/admins/types/index.ts
similarity index 100%
rename from dashboard/src/features/admins/types/index.ts
rename to dashboard/src/modules/admins/types/index.ts
diff --git a/dashboard/src/features/admins/types/schema.ts b/dashboard/src/modules/admins/types/schema.ts
similarity index 100%
rename from dashboard/src/features/admins/types/schema.ts
rename to dashboard/src/modules/admins/types/schema.ts
diff --git a/dashboard/src/routes/_dashboard/admins.lazy.tsx b/dashboard/src/routes/_dashboard/admins.lazy.tsx
index 881498b..9f334e5 100644
--- a/dashboard/src/routes/_dashboard/admins.lazy.tsx
+++ b/dashboard/src/routes/_dashboard/admins.lazy.tsx
@@ -8,7 +8,7 @@ import {
} from '@marzneshin/components';
import { SudoRoute } from '@marzneshin/features/sudo-routes'
import { createFileRoute, Outlet } from '@tanstack/react-router'
-import { AdminsTable } from '@marzneshin/features/admins'
+import { AdminsTable } from '@marzneshin/modules/admins'
import { type FC, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
diff --git a/dashboard/src/routes/_dashboard/admins/$adminId.tsx b/dashboard/src/routes/_dashboard/admins/$adminId.tsx
index 828be3b..0786638 100644
--- a/dashboard/src/routes/_dashboard/admins/$adminId.tsx
+++ b/dashboard/src/routes/_dashboard/admins/$adminId.tsx
@@ -7,7 +7,7 @@ import { queryClient } from "@marzneshin/utils";
import {
RouterAdminContext,
adminQueryOptions,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
import { Suspense, useMemo } from "react";
import {
AlertDialog,
diff --git a/dashboard/src/routes/_dashboard/admins/$adminId/delete.tsx b/dashboard/src/routes/_dashboard/admins/$adminId/delete.tsx
index c03b955..dfb9609 100644
--- a/dashboard/src/routes/_dashboard/admins/$adminId/delete.tsx
+++ b/dashboard/src/routes/_dashboard/admins/$adminId/delete.tsx
@@ -5,7 +5,7 @@ import {
import {
AdminsDeleteConfirmationDialog,
useRouterAdminContext,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
import { useDialog } from "@marzneshin/hooks";
const AdminDelete = () => {
diff --git a/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx b/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx
index ff0347a..65886df 100644
--- a/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx
+++ b/dashboard/src/routes/_dashboard/admins/$adminId/edit.tsx
@@ -5,7 +5,7 @@ import {
import {
AdminsMutationDialog,
useRouterAdminContext,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
const AdminEdit = () => {
const value = useRouterAdminContext()
diff --git a/dashboard/src/routes/_dashboard/admins/$adminId/index.tsx b/dashboard/src/routes/_dashboard/admins/$adminId/index.tsx
index f1a750f..30efdf7 100644
--- a/dashboard/src/routes/_dashboard/admins/$adminId/index.tsx
+++ b/dashboard/src/routes/_dashboard/admins/$adminId/index.tsx
@@ -5,7 +5,7 @@ import {
import {
useRouterAdminContext,
AdminsSettingsDialog,
-} from "@marzneshin/features/admins";
+} from "@marzneshin/modules/admins";
import { useDialog } from "@marzneshin/hooks";
const AdminOpen = () => {
diff --git a/dashboard/src/routes/_dashboard/admins/create.tsx b/dashboard/src/routes/_dashboard/admins/create.tsx
index 63adfb2..4d39cb3 100644
--- a/dashboard/src/routes/_dashboard/admins/create.tsx
+++ b/dashboard/src/routes/_dashboard/admins/create.tsx
@@ -1,5 +1,5 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
-import { AdminsMutationDialog } from "@marzneshin/features/admins";
+import { AdminsMutationDialog } from "@marzneshin/modules/admins";
const AdminCreate = () => {
const navigate = useNavigate({ from: "/admins/create" });
diff --git a/package.json b/package.json
deleted file mode 100644
index 33c4fee..0000000
--- a/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "dependencies": {
- "yaml": "^2.5.0"
- }
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
deleted file mode 100644
index fc98ac4..0000000
--- a/pnpm-lock.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-lockfileVersion: '9.0'
-
-settings:
- autoInstallPeers: true
- excludeLinksFromLockfile: false
-
-importers:
-
- .:
- dependencies:
- yaml:
- specifier: ^2.5.0
- version: 2.5.0
-
-packages:
-
- yaml@2.5.0:
- resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==}
- engines: {node: '>= 14'}
- hasBin: true
-
-snapshots:
-
- yaml@2.5.0: {}