Files
oneuptime/Common/UI/Components/ModelTable/BaseModelTable.tsx

2157 lines
65 KiB
TypeScript

import Includes from "../../../Types/BaseDatabase/Includes";
import { API_DOCS_URL, BILLING_ENABLED, getAllEnvVars } from "../../Config";
import { GetReactElementFunction } from "../../Types/FunctionTypes";
import SelectEntityField from "../../Types/SelectEntityField";
import API from "../../Utils/API/API";
import Query from "../../../Types/BaseDatabase/Query";
import GroupBy from "../../../Types/BaseDatabase/GroupBy";
import Sort from "../../../Types/BaseDatabase/Sort";
import Select from "../../../Types/BaseDatabase/Select";
import { Logger } from "../../Utils/Logger";
import Navigation from "../../Utils/Navigation";
import PermissionUtil from "../../Utils/Permission";
import ProjectUtil from "../../Utils/Project";
import User from "../../Utils/User";
import ActionButtonSchema from "../ActionButton/ActionButtonSchema";
import {
BulkActionButtonSchema,
BulkActionFailed,
BulkActionOnClickProps,
} from "../BulkUpdate/BulkUpdateForm";
import { ButtonSize, ButtonStyleType } from "../Button/Button";
import Card, {
CardButtonSchema,
ComponentProps as CardComponentProps,
} from "../Card/Card";
import { getRefreshButton } from "../Card/CardButtons/Refresh";
import Field from "../Detail/Field";
import ErrorMessage from "../ErrorMessage/ErrorMessage";
import ClassicFilterType from "../Filters/Types/Filter";
import FilterData from "../Filters/Types/FilterData";
import { FormProps, FormSummaryConfig } from "../Forms/BasicForm";
import { ModelField } from "../Forms/ModelForm";
import { FormStep } from "../Forms/Types/FormStep";
import FormValues from "../Forms/Types/FormValues";
import List from "../List/List";
import { ListDetailProps } from "../List/ListRow";
import ConfirmModal from "../Modal/ConfirmModal";
import Modal, { ModalWidth } from "../Modal/Modal";
import MarkdownViewer from "../Markdown.tsx/MarkdownViewer";
import Filter from "../ModelFilter/Filter";
import { DropdownOption, DropdownOptionLabel } from "../Dropdown/Dropdown";
import OrderedStatesList from "../OrderedStatesList/OrderedStatesList";
import Pill from "../Pill/Pill";
import Table from "../Table/Table";
import TableColumn from "../Table/Types/Column";
import FieldType from "../Types/FieldType";
import ModelTableColumn from "./Column";
import Columns from "./Columns";
import AnalyticsBaseModel, {
AnalyticsBaseModelType,
} from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
import BaseModel, {
DatabaseBaseModelType,
} from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
import AccessControlModel from "../../../Models/DatabaseModels/DatabaseBaseModel/AccessControlModel";
import Route from "../../../Types/API/Route";
import URL from "../../../Types/API/URL";
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
import InBetween from "../../../Types/BaseDatabase/InBetween";
import Search from "../../../Types/BaseDatabase/Search";
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
import SubscriptionPlan, {
PlanType,
} from "../../../Types/Billing/SubscriptionPlan";
import { Yellow } from "../../../Types/BrandColors";
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
import Dictionary from "../../../Types/Dictionary";
import BadDataException from "../../../Types/Exception/BadDataException";
import Color from "../../../Types/Color";
import {
ErrorFunction,
PromiseVoidFunction,
VoidFunction,
} from "../../../Types/FunctionTypes";
import IconProp from "../../../Types/Icon/IconProp";
import { JSONObject } from "../../../Types/JSON";
import ObjectID from "../../../Types/ObjectID";
import Permission, {
PermissionHelper,
UserPermission,
} from "../../../Types/Permission";
import Typeof from "../../../Types/Typeof";
import React, {
MutableRefObject,
ReactElement,
useEffect,
useState,
} from "react";
import TableViewElement from "./TableView";
import TableView from "../../../Models/DatabaseModels/TableView";
import UserPreferences, {
UserPreferenceType,
} from "../../../Utils/UserPreferences";
import RequestOptions from "../../Utils/API/RequestOptions";
import ListResult from "../../../Types/BaseDatabase/ListResult";
export enum ShowAs {
Table,
List,
OrderedStatesList,
}
export interface SaveFilterProps {
tableId: string;
}
export interface BaseTableCallbacks<
TBaseModel extends BaseModel | AnalyticsBaseModel,
> {
deleteItem: (item: TBaseModel) => Promise<void>;
getModelFromJSON: (item: JSONObject) => TBaseModel;
getJSONFromModel: (item: TBaseModel) => JSONObject;
addSlugToSelect: (select: Select<TBaseModel>) => Select<TBaseModel>;
getList: (data: {
modelType: DatabaseBaseModelType | AnalyticsBaseModelType;
query: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
limit: number;
skip: number;
sort: Sort<TBaseModel>;
select: Select<TBaseModel>;
requestOptions?: RequestOptions | undefined;
}) => Promise<ListResult<TBaseModel>>;
toJSONArray: (data: Array<TBaseModel>) => Array<JSONObject>;
updateById: (data: { id: ObjectID; data: JSONObject }) => Promise<void>;
showCreateEditModal: (data: {
modalType: ModalType;
modelIdToEdit?: ObjectID | undefined;
onBeforeCreate?:
| ((item: TBaseModel, miscDataProps: JSONObject) => Promise<TBaseModel>)
| undefined;
onSuccess?: ((item: TBaseModel) => void) | undefined;
onClose?: (() => void) | undefined;
}) => ReactElement;
}
export enum ModalTableBulkDefaultActions {
Delete = "Delete",
}
export interface BulkActionProps<T extends BaseModel | AnalyticsBaseModel> {
buttons: Array<BulkActionButtonSchema<T> | ModalTableBulkDefaultActions>;
matchBulkSelectedItemByField?: keyof T | undefined; // which field to use to match selected items. For exmaple this could be '_id'
}
export interface BaseTableProps<
TBaseModel extends BaseModel | AnalyticsBaseModel,
> {
modelType: { new (): TBaseModel };
id: string;
onFetchInit?: undefined | ((pageNumber: number, itemsOnPage: number) => void);
onFetchSuccess?:
| undefined
| ((data: Array<TBaseModel>, totalCount: number) => void);
cardProps?: CardComponentProps | undefined;
helpContent?:
| {
title: string;
description?: string | undefined;
markdown: string;
}
| undefined;
documentationLink?: Route | URL | undefined;
videoLink?: Route | URL | undefined;
showCreateForm?: undefined | boolean;
columns: Columns<TBaseModel>;
filters: Array<Filter<TBaseModel>>;
listDetailOptions?: undefined | ListDetailProps;
selectMoreFields?: Select<TBaseModel>;
initialItemsOnPage?: number;
isDeleteable: boolean;
isEditable?: boolean | undefined;
isCreateable: boolean;
disablePagination?: undefined | boolean;
formFields?: undefined | Array<ModelField<TBaseModel>>;
formSteps?: undefined | Array<FormStep<TBaseModel>>;
noItemsMessage?: undefined | string | ReactElement;
showRefreshButton?: undefined | boolean;
isViewable?: undefined | boolean;
showViewIdButton?: undefined | boolean;
enableDragAndDrop?: boolean | undefined;
viewPageRoute?: undefined | Route;
onViewPage?: (item: TBaseModel) => Promise<Route | URL>;
query?: Query<TBaseModel>;
groupBy?: GroupBy<TBaseModel> | undefined;
onBeforeFetch?: (() => Promise<TBaseModel>) | undefined;
createInitialValues?: FormValues<TBaseModel> | undefined;
onBeforeCreate?:
| ((item: TBaseModel, miscDataProps: JSONObject) => Promise<TBaseModel>)
| undefined;
onCreateSuccess?: ((item: TBaseModel) => Promise<TBaseModel>) | undefined;
createVerb?: string;
showAs?: ShowAs | undefined;
singularName?: string | undefined;
pluralName?: string | undefined;
actionButtons?: Array<ActionButtonSchema<TBaseModel>> | undefined;
deleteButtonText?: string | undefined;
onCreateEditModalClose?: (() => void) | undefined;
editButtonText?: string | undefined;
viewButtonText?: string | undefined;
refreshToggle?: string | undefined;
fetchRequestOptions?: RequestOptions | undefined;
deleteRequestOptions?: RequestOptions | undefined;
onItemDeleted?: ((item: TBaseModel) => void) | undefined;
onBeforeEdit?: ((item: TBaseModel) => Promise<TBaseModel>) | undefined;
onBeforeDelete?: ((item: TBaseModel) => Promise<TBaseModel>) | undefined;
onBeforeView?: ((item: TBaseModel) => Promise<TBaseModel>) | undefined;
sortBy?: keyof TBaseModel | undefined;
sortOrder?: SortOrder | undefined;
dragDropIdField?: keyof TBaseModel | undefined;
dragDropIndexField?: keyof TBaseModel | undefined;
createEditModalWidth?: ModalWidth | undefined;
orderedStatesListProps?: {
titleField: keyof TBaseModel;
descriptionField?: keyof TBaseModel | undefined;
orderField: keyof TBaseModel;
shouldAddItemInTheEnd?: boolean;
shouldAddItemInTheBeginning?: boolean;
};
onViewComplete?: ((item: TBaseModel) => void) | undefined;
createEditFromRef?:
| undefined
| MutableRefObject<FormProps<FormValues<TBaseModel>>>;
name: string;
// bulk actions
bulkActions?: BulkActionProps<TBaseModel> | undefined;
onShowFormType?: (formType: ModalType) => void;
initialFilterData?: FilterData<TBaseModel> | undefined;
saveFilterProps?: SaveFilterProps | undefined;
onFilterApplied?: ((isFilterApplied: boolean) => void) | undefined;
formSummary?: FormSummaryConfig | undefined;
onAdvancedFiltersToggle?:
| undefined
| ((showAdvancedFilters: boolean) => void);
/*
* this key is used to save table user preferences in local storage.
* If you provide this key, the table will save the user preferences in local storage.
* If you do not provide this key, the table will not save the user preferences in local storage.
*/
userPreferencesKey: string;
}
export interface ComponentProps<
TBaseModel extends BaseModel | AnalyticsBaseModel,
> extends BaseTableProps<TBaseModel> {
callbacks: BaseTableCallbacks<TBaseModel>;
}
export enum ModalType {
Create,
Edit,
}
const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
props: ComponentProps<TBaseModel>,
) => ReactElement = <TBaseModel extends BaseModel | AnalyticsBaseModel>(
props: ComponentProps<TBaseModel>,
): ReactElement => {
const [tableView, setTableView] = useState<TableView | null>(null);
const matchBulkSelectedItemByField: keyof TBaseModel =
props.bulkActions?.matchBulkSelectedItemByField || "_id";
const model: TBaseModel = new props.modelType();
const [bulkSelectedItems, setBulkSelectedItems] = useState<Array<TBaseModel>>(
[],
);
let showAs: ShowAs | undefined = props.showAs;
if (!showAs) {
showAs = ShowAs.Table;
}
const propsQueryRef: React.MutableRefObject<Query<TBaseModel>> = React.useRef(
props.query || {},
);
const [showViewIdModal, setShowViewIdModal] = useState<boolean>(false);
const [showHelpModal, setShowHelpModal] = useState<boolean>(false);
const [viewId, setViewId] = useState<string | null>(null);
const [tableColumns, setColumns] = useState<Array<TableColumn<TBaseModel>>>(
[],
);
const [classicTableFilters, setClassicTableFilters] = useState<
Array<ClassicFilterType<TBaseModel>>
>([]);
const [cardButtons, setCardButtons] = useState<
Array<CardButtonSchema | ReactElement>
>([]);
const [actionButtonSchema, setActionButtonSchema] = useState<
Array<ActionButtonSchema<TBaseModel>>
>([]);
useEffect(() => {
if (props.showCreateForm) {
setShowModal(true);
setModalType(ModalType.Create);
}
}, [props.showCreateForm]);
useEffect(() => {
if (props.onShowFormType) {
props.onShowFormType(modalType);
}
}, [props.modelType]);
const getItemsOnPage: () => number = (): number => {
if (props.userPreferencesKey) {
const itemsOnPage: number | null =
UserPreferences.getUserPreferenceByTypeAsNumber({
userPreferenceType: UserPreferenceType.BaseModelTablePageSize,
key: props.userPreferencesKey,
});
if (itemsOnPage) {
return itemsOnPage;
}
}
return props.initialItemsOnPage || 10;
};
const [orderedStatesListNewItemOrder, setOrderedStatesListNewItemOrder] =
useState<number | null>(null);
const [onBeforeFetchData, setOnBeforeFetchData] = useState<
TBaseModel | undefined
>(undefined);
const [data, setData] = useState<Array<TBaseModel>>([]);
const [query, setQuery] = useState<Query<TBaseModel>>({});
const [currentPageNumber, setCurrentPageNumber] = useState<number>(1);
const [totalItemsCount, setTotalItemsCount] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const [tableFilterError, setTableFilterError] = useState<string>("");
const [showModel, setShowModal] = useState<boolean>(false);
const [showFilterModal, setShowFilterModal] = useState<boolean>(false);
const [modalType, setModalType] = useState<ModalType>(ModalType.Create);
const [sortBy, setSortBy] = useState<keyof TBaseModel | null>(
props.sortBy || null,
);
const [sortOrder, setSortOrder] = useState<SortOrder>(
props.sortOrder || SortOrder.Ascending,
);
const [showDeleteConfirmModal, setShowDeleteConfirmModal] =
useState<boolean>(false);
const [currentEditableItem, setCurrentEditableItem] =
useState<TBaseModel | null>(null);
const [currentDeleteableItem, setCurrentDeleteableItem] =
useState<TBaseModel | null>(null);
const [itemsOnPage, setItemsOnPage] = useState<number>(getItemsOnPage());
useEffect(() => {
// update items on page in localstorage.
if (itemsOnPage && props.userPreferencesKey) {
UserPreferences.saveUserPreferenceByTypeAsNumber({
userPreferenceType: UserPreferenceType.BaseModelTablePageSize,
key: props.userPreferencesKey,
value: itemsOnPage,
});
}
}, [itemsOnPage]);
const [fields, setFields] = useState<Array<Field<TBaseModel>>>([]);
const [isFilterFetchLoading, setIsFilterFetchLoading] = useState(false);
const [errorModalText, setErrorModalText] = useState<string>("");
useEffect(() => {
const currentQuery: Query<TBaseModel> = propsQueryRef.current;
const newQuery: Query<TBaseModel> = props.query || {};
// only update if the query has changed.
if (JSON.stringify(currentQuery) !== JSON.stringify(newQuery)) {
propsQueryRef.current = newQuery;
fetchItems().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}
}, [props.query]);
useEffect(() => {
if (!showModel) {
props.onCreateEditModalClose?.();
}
}, [showModel]);
useEffect(() => {
const detailFields: Array<Field<TBaseModel>> = [];
for (const column of tableColumns) {
if (!column.key) {
// if its an action column, ignore.
continue;
}
detailFields.push({
title: column.title,
description: column.description || "",
key: column.key as keyof TBaseModel,
fieldType: column.type,
colSpan: column.colSpan,
contentClassName: column.contentClassName,
alignItem: column.alignItem,
hideOnMobile: column.hideOnMobile, // Propagate hideOnMobile property
getElement: column.getElement
? (item: TBaseModel): ReactElement => {
return column.getElement!(item, onBeforeFetchData);
}
: undefined,
});
setFields(detailFields);
}
}, [tableColumns]);
type GetRelationSelectFunction = () => Select<TBaseModel>;
const getRelationSelect: GetRelationSelectFunction =
(): Select<TBaseModel> => {
const relationSelect: Select<TBaseModel> = {};
for (const column of props.columns || []) {
const key: string | null = column.field
? (Object.keys(column.field)[0] as string)
: null;
if (key && model.isFileColumn(key)) {
(relationSelect as JSONObject)[key] = {
file: true,
_id: true,
fileType: true,
name: true,
};
} else if (key && model.isEntityColumn(key)) {
if (!(relationSelect as JSONObject)[key]) {
(relationSelect as JSONObject)[key] = {};
}
(relationSelect as JSONObject)[key] = {
...((relationSelect as JSONObject)[key] as JSONObject),
...(column.field as any)[key],
};
}
}
return relationSelect;
};
type DeleteItemFunction = (item: TBaseModel) => Promise<void>;
const deleteItem: DeleteItemFunction = async (item: TBaseModel) => {
if (!item.id) {
throw new BadDataException("item.id cannot be null");
}
setIsLoading(true);
try {
await props.callbacks.deleteItem(item);
props.onItemDeleted?.(item);
if (data.length === 1 && currentPageNumber > 1) {
setCurrentPageNumber(currentPageNumber - 1);
}
await fetchItems();
} catch (err) {
setErrorModalText(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
const serializeToTableColumns: VoidFunction = (): void => {
// Convert ModelColumns to TableColumns.
const columns: Array<TableColumn<TBaseModel>> = [];
let selectFields: Select<TBaseModel> = {
_id: true,
};
selectFields = props.callbacks.addSlugToSelect(selectFields);
const userPermissions: Array<Permission> = getUserPermissions();
const accessControl: Dictionary<ColumnAccessControl> =
model.getColumnAccessControlForAllColumns();
for (const column of props.columns || []) {
const hasPermission: boolean =
hasPermissionToReadColumn(column) || User.isMasterAdmin();
const key: keyof TBaseModel | null = getColumnKey(column);
if (hasPermission) {
let tooltipText: ((item: TBaseModel) => string) | undefined = undefined;
if (column.tooltipText) {
tooltipText = (item: TBaseModel): string => {
if (
item instanceof BaseModel ||
item instanceof AnalyticsBaseModel
) {
return column.tooltipText!(item);
}
return column.tooltipText!(
props.callbacks.getModelFromJSON(item as JSONObject),
);
};
}
// get filter options if they were already loaded
const columnKey: keyof TBaseModel | null = column.selectedProperty
? (((key as string) +
"." +
column.selectedProperty) as keyof TBaseModel)
: key;
columns.push({
...column,
disableSort: column.disableSort || shouldDisableSort(key),
key: columnKey,
tooltipText,
getElement: column.getElement,
});
if (key) {
selectFields[key] = true;
}
}
}
const selectMoreFields: Array<string> = props.selectMoreFields
? Object.keys(props.selectMoreFields)
: [];
for (const moreField of selectMoreFields) {
let hasPermissionToSelectField: boolean = true;
let fieldPermissions: Array<Permission> = [];
fieldPermissions = accessControl[moreField as string]?.read || [];
if (
accessControl[moreField]?.read &&
!PermissionHelper.doesPermissionsIntersect(
userPermissions,
fieldPermissions,
)
) {
hasPermissionToSelectField = false;
}
if (hasPermissionToSelectField) {
(selectFields as Dictionary<boolean>)[moreField] = true;
} else {
Logger.warn(
"User does not have read permissions to read - " + moreField,
);
}
}
const permissions: Array<Permission> | null =
PermissionUtil.getAllPermissions();
let showActionsColumn: boolean = Boolean(
(permissions &&
((props.isDeleteable && model.hasDeletePermissions(permissions)) ||
(props.isEditable && model.hasUpdatePermissions(permissions)) ||
(props.isViewable && model.hasReadPermissions(permissions)))) ||
(props.actionButtons && props.actionButtons.length > 0) ||
props.showViewIdButton,
);
if (User.isMasterAdmin()) {
if (
(props.actionButtons && props.actionButtons.length > 0) ||
props.showViewIdButton ||
props.isDeleteable ||
props.isEditable ||
props.isViewable
) {
showActionsColumn = true;
}
}
if (showActionsColumn) {
columns.push({
title: "Actions",
type: FieldType.Actions,
});
}
setActionSchema();
setHeaderButtons();
setColumns(columns);
};
const getFilterDropdownItems: PromiseVoidFunction =
async (): Promise<void> => {
setTableFilterError("");
setIsFilterFetchLoading(true);
const filters: Array<Filter<TBaseModel>> = [...props.filters];
try {
for (const filter of filters) {
const key: keyof TBaseModel | null = getFilterKey(filter);
if (!key) {
continue;
}
if (!filter.filterEntityType) {
continue;
}
if (!filter.filterDropdownField) {
Logger.warn(
`Cannot filter on ${key.toString()} because filter.dropdownField is not set.`,
);
continue;
}
const hasPermission: boolean = hasPermissionToReadFilter(filter);
if (!hasPermission) {
continue;
}
if (filter.fetchFilterDropdownOptions) {
// fetch filter dropdown options.
filter.filterDropdownOptions =
await filter.fetchFilterDropdownOptions();
continue;
}
const query: Query<TBaseModel> = filter.filterQuery || {};
let colorColumnName: string | null = null;
let accessControlColumnName: string | null = null;
if (
filter.filterEntityType &&
filter.filterEntityType.prototype instanceof BaseModel
) {
const filterModel: BaseModel =
new (filter.filterEntityType as DatabaseBaseModelType)();
colorColumnName = filterModel.getFirstColorColumn();
accessControlColumnName = filterModel.getAccessControlColumn();
}
const select: Select<TBaseModel> = {
[filter.filterDropdownField.label]: true,
[filter.filterDropdownField.value]: true,
} as Select<TBaseModel>;
if (colorColumnName) {
(select as Dictionary<boolean>)[colorColumnName] = true;
}
if (accessControlColumnName) {
(select as Dictionary<JSONObject>)[accessControlColumnName] = {
_id: true,
name: true,
color: true,
} as JSONObject;
}
const listResult: ListResult<TBaseModel> =
await props.callbacks.getList({
modelType: filter.filterEntityType,
query: query,
limit: LIMIT_PER_PROJECT,
skip: 0,
select: select,
sort: {},
});
filter.filterDropdownOptions = [];
for (const item of listResult.data) {
const option: DropdownOption = {
value: item.getColumnValue(
filter.filterDropdownField.value,
) as string,
label: item.getColumnValue(
filter.filterDropdownField.label,
) as string,
};
if (colorColumnName) {
const colorValue: Color | string | null = item.getColumnValue(
colorColumnName,
) as Color | string | null;
if (colorValue instanceof Color) {
option.color = colorValue;
} else if (
typeof colorValue === "string" &&
colorValue.trim().length > 0
) {
try {
option.color = new Color(colorValue);
} catch {
// ignore invalid colors
}
}
}
if (accessControlColumnName) {
const accessControlValue:
| AccessControlModel
| Array<AccessControlModel>
| null =
(item.getColumnValue(accessControlColumnName) as
| AccessControlModel
| Array<AccessControlModel>
| null) || null;
const accessControlItems: Array<AccessControlModel> =
Array.isArray(accessControlValue)
? accessControlValue
: accessControlValue
? [accessControlValue]
: [];
type SimplifiedDropdownLabel = {
id?: string;
name: string;
color?: Color;
};
const dropdownLabels: Array<SimplifiedDropdownLabel> =
accessControlItems
.map((label: AccessControlModel | null) => {
if (!label) {
return null;
}
const labelNameRaw: string | null = label.getColumnValue(
"name",
) as string | null;
if (!labelNameRaw) {
return null;
}
const labelName: string = labelNameRaw.toString().trim();
if (!labelName) {
return null;
}
const labelColorValue: Color | null = label.getColumnValue(
"color",
) as Color | null;
const normalizedLabel: SimplifiedDropdownLabel = {
name: labelName,
};
const labelId: ObjectID | null = label.id;
if (labelId) {
normalizedLabel.id = labelId.toString();
}
if (labelColorValue) {
normalizedLabel.color = labelColorValue;
}
return normalizedLabel;
})
.filter(
(
label: SimplifiedDropdownLabel | null,
): label is SimplifiedDropdownLabel => {
return label !== null;
},
);
if (dropdownLabels.length > 0) {
option.labels = dropdownLabels as Array<DropdownOptionLabel>;
}
}
filter.filterDropdownOptions.push(option);
}
}
const classicFilters: Array<ClassicFilterType<TBaseModel>> = filters
.map((filter: Filter<TBaseModel>) => {
const key: keyof TBaseModel | null = getFilterKey(filter);
if (!key) {
return null;
}
return {
title: filter.title,
filterDropdownOptions: filter.filterDropdownOptions,
key: key,
type: filter.type,
jsonKeys: filter.jsonKeys,
isAdvancedFilter: filter.isAdvancedFilter,
};
})
.filter((filter: ClassicFilterType<TBaseModel> | null) => {
return filter !== null;
}) as Array<ClassicFilterType<TBaseModel>>;
setClassicTableFilters(classicFilters);
setHeaderButtons();
} catch (err) {
setTableFilterError(API.getFriendlyMessage(err));
}
setIsFilterFetchLoading(false);
};
const fetchAllBulkItems: PromiseVoidFunction = async (): Promise<void> => {
setError("");
setIsLoading(true);
try {
const listResult: ListResult<TBaseModel> = await props.callbacks.getList({
modelType: props.modelType as
| DatabaseBaseModelType
| AnalyticsBaseModelType,
query: {
...props.query,
...query,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
select: {
_id: true,
},
sort: {},
requestOptions: props.fetchRequestOptions,
});
setBulkSelectedItems(listResult.data);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
const fetchItems: PromiseVoidFunction = async (): Promise<void> => {
setError("");
setIsLoading(true);
if (props.onFetchInit) {
props.onFetchInit(currentPageNumber, itemsOnPage);
}
if (props.onBeforeFetch) {
const model: TBaseModel = await props.onBeforeFetch();
setOnBeforeFetchData(model);
}
try {
const listResult: ListResult<TBaseModel> = await props.callbacks.getList({
modelType: props.modelType as
| DatabaseBaseModelType
| AnalyticsBaseModelType,
query: {
...props.query,
...query,
},
groupBy: {
...props.groupBy,
},
limit: itemsOnPage,
skip: (currentPageNumber - 1) * itemsOnPage,
select: {
...getSelect(),
...getRelationSelect(),
},
sort: sortBy
? {
[sortBy as any]: sortOrder,
}
: {},
requestOptions: props.fetchRequestOptions,
});
setTotalItemsCount(listResult.count);
setData(listResult.data);
} catch (err) {
setError(API.getFriendlyMessage(err));
}
setIsLoading(false);
};
useEffect(() => {
if (showFilterModal) {
getFilterDropdownItems().catch((err: Error) => {
setTableFilterError(API.getFriendlyMessage(err));
});
}
}, [showFilterModal, props.filters]);
type GetSelectFunction = () => Select<TBaseModel>;
const getSelect: GetSelectFunction = (): Select<TBaseModel> => {
const selectFields: Select<TBaseModel> = {
_id: true,
};
for (const column of props.columns || []) {
const key: string | null = column.field
? (Object.keys(column.field)[0] as string)
: null;
if (key) {
if (model.hasColumn(key)) {
(selectFields as Dictionary<boolean>)[key] = true;
} else {
throw new BadDataException(
`${key} column not found on ${model.singularName}`,
);
}
}
}
const selectMoreFields: Array<keyof TBaseModel> = props.selectMoreFields
? (Object.keys(props.selectMoreFields) as Array<keyof TBaseModel>)
: [];
if (props.dragDropIndexField) {
selectMoreFields.push(props.dragDropIndexField);
}
if (
props.dragDropIdField &&
!Object.keys(selectFields).includes(props.dragDropIdField as string) &&
!selectMoreFields.includes(props.dragDropIdField)
) {
selectMoreFields.push(props.dragDropIdField);
}
for (const moreField of selectMoreFields) {
if (
model.hasColumn(moreField as string) &&
model.isEntityColumn(moreField as string)
) {
(selectFields as Dictionary<boolean>)[moreField as string] = (
props.selectMoreFields as any
)[moreField];
} else if (model.hasColumn(moreField as string)) {
(selectFields as Dictionary<boolean>)[moreField as string] = true;
} else {
throw new BadDataException(
`${moreField as string} column not found on ${model.singularName}`,
);
}
}
return selectFields;
};
const getSaveFilterDropdown: GetReactElementFunction = (): ReactElement => {
if (!props.saveFilterProps) {
return <></>;
}
if (props.saveFilterProps && props.saveFilterProps.tableId) {
return (
<TableViewElement
tableId={props.saveFilterProps.tableId}
tableView={tableView}
currentQuery={query}
currentSortBy={sortBy}
currentItemsOnPage={itemsOnPage}
currentSortOrder={sortOrder}
onViewChange={async (tableView: TableView | null) => {
setTableView(tableView);
if (tableView) {
const sortBy: string | undefined = Object.keys(
tableView.sort || {},
)[0];
let sortOrder: SortOrder = SortOrder.Descending;
if (sortBy && tableView.sort) {
sortOrder =
((tableView.sort as any)[sortBy as any] as any) ||
SortOrder.Descending;
}
// then set query, sort and items on the page
setQuery(tableView.query || {});
setFilterData(tableView.query || {});
setItemsOnPage(tableView.itemsOnPage || 10);
setSortBy((sortBy as keyof TBaseModel) || null);
setSortOrder(sortOrder);
if (classicTableFilters.length === 0) {
await getFilterDropdownItems();
}
} else {
setQuery({});
setSortBy(null);
setSortOrder(SortOrder.Descending);
setItemsOnPage(10);
}
}}
/>
);
}
return <></>;
};
const setHeaderButtons: VoidFunction = (): void => {
// add header buttons.
let headerbuttons: Array<CardButtonSchema | ReactElement> = [];
// Add documentation link button first if provided
if (props.documentationLink) {
headerbuttons.push({
title: "View Documentation",
icon: IconProp.Book,
buttonStyle: ButtonStyleType.HOVER_PRIMARY_OUTLINE,
buttonSize: ButtonSize.Small,
className: "hidden md:flex",
onClick: () => {
Navigation.navigate(props.documentationLink!, {
openInNewTab: true,
});
},
});
}
// Add help content button if provided
if (props.helpContent) {
headerbuttons.push({
title: "",
icon: IconProp.Help,
buttonStyle: ButtonStyleType.ICON,
buttonSize: ButtonSize.Small,
className: "",
onClick: () => {
setShowHelpModal(true);
},
});
}
// Add video link button if provided
if (props.videoLink) {
headerbuttons.push({
title: "Watch Demo",
icon: IconProp.Play,
buttonStyle: ButtonStyleType.HOVER_PRIMARY_OUTLINE,
buttonSize: ButtonSize.Small,
className: "hidden md:flex",
onClick: () => {
Navigation.navigate(props.videoLink!, {
openInNewTab: true,
});
},
});
}
if (props.cardProps?.buttons && props.cardProps?.buttons.length > 0) {
headerbuttons = [...headerbuttons, ...props.cardProps.buttons];
}
const permissions: Array<Permission> | null =
PermissionUtil.getAllPermissions();
let hasPermissionToCreate: boolean = false;
if (permissions) {
hasPermissionToCreate =
model.hasCreatePermissions(permissions) || User.isMasterAdmin();
}
const showFilterButton: boolean = props.filters.length > 0;
// because ordered list add button is inside the table and not on the card header.
if (
props.isCreateable &&
hasPermissionToCreate &&
showAs !== ShowAs.OrderedStatesList
) {
headerbuttons.push({
title: `${props.createVerb || "Create"} ${
props.singularName || model.singularName
}`,
buttonStyle: ButtonStyleType.NORMAL,
buttonSize: ButtonSize.Normal,
className: "",
onClick: () => {
setModalType(ModalType.Create);
setShowModal(true);
},
icon: IconProp.Add,
});
}
if (props.showRefreshButton) {
headerbuttons.push({
...getRefreshButton(),
buttonSize: ButtonSize.Small,
className: "",
onClick: async () => {
await fetchItems();
},
disabled: isFilterFetchLoading,
});
}
if (showFilterButton) {
headerbuttons.push({
title: "",
buttonStyle: ButtonStyleType.ICON,
buttonSize: ButtonSize.Small,
className: "",
onClick: () => {
setQuery({});
setShowFilterModal(true);
},
disabled: isFilterFetchLoading,
icon: IconProp.Filter,
});
}
if (props.saveFilterProps) {
headerbuttons.push(getSaveFilterDropdown());
}
setCardButtons(headerbuttons);
};
useEffect(() => {
fetchItems().catch((err: Error) => {
setError(API.getFriendlyMessage(err));
});
}, [
currentPageNumber,
sortBy,
sortOrder,
itemsOnPage,
query,
props.refreshToggle,
]);
type ShouldDisableSortFunction = (
columnName: keyof TBaseModel | null,
) => boolean;
const shouldDisableSort: ShouldDisableSortFunction = (
columnName: keyof TBaseModel | null,
): boolean => {
if (!columnName) {
return true;
}
return model.isEntityColumn(columnName as string);
};
type GetColumnKeyFunction = (
column: ModelTableColumn<TBaseModel>,
) => keyof TBaseModel | null;
const getColumnKey: GetColumnKeyFunction = (
column: ModelTableColumn<TBaseModel>,
): keyof TBaseModel | null => {
const key: keyof TBaseModel | null = column.field
? (Object.keys(column.field)[0] as keyof TBaseModel)
: null;
return key;
};
type GetFilterKeyFunction = (
filter: Filter<TBaseModel>,
) => keyof TBaseModel | null;
const getFilterKey: GetFilterKeyFunction = (
filter: Filter<TBaseModel>,
): keyof TBaseModel | null => {
const key: keyof TBaseModel | null = filter.field
? (Object.keys(filter.field)[0] as keyof TBaseModel)
: null;
return key;
};
type HasPermissionToReadColumnFunction = (
column: ModelTableColumn<TBaseModel>,
) => boolean;
const hasPermissionToReadColumn: HasPermissionToReadColumnFunction = (
column: ModelTableColumn<TBaseModel>,
): boolean => {
const key: keyof TBaseModel | null = getColumnKey(column);
if (!key) {
return true;
}
return hasPermissionToReadField(key);
};
type HasPermissionToReadFilterColumn = (
filter: Filter<TBaseModel>,
) => boolean;
const hasPermissionToReadFilter: HasPermissionToReadFilterColumn = (
filter: Filter<TBaseModel>,
): boolean => {
const key: keyof TBaseModel | null = getFilterKey(filter);
if (!key) {
return true;
}
return hasPermissionToReadField(key);
};
type HasPermissionToReadFieldFunction = (field: keyof TBaseModel) => boolean;
const hasPermissionToReadField: HasPermissionToReadFieldFunction = (
field: keyof TBaseModel,
): boolean => {
const accessControl: Dictionary<ColumnAccessControl> =
model.getColumnAccessControlForAllColumns();
const userPermissions: Array<Permission> = getUserPermissions();
const key: keyof TBaseModel = field;
// check permissions.
let hasPermission: boolean = false;
if (!key) {
hasPermission = true;
}
if (key) {
hasPermission = true;
let fieldPermissions: Array<Permission> = [];
fieldPermissions = accessControl[key as string]?.read || [];
if (
accessControl[key]?.read &&
!PermissionHelper.doesPermissionsIntersect(
userPermissions,
fieldPermissions,
)
) {
hasPermission = false;
}
}
return hasPermission;
};
type GetUserPermissionsFunction = () => Array<Permission>;
const getUserPermissions: GetUserPermissionsFunction =
(): Array<Permission> => {
let userPermissions: Array<Permission> =
PermissionUtil.getGlobalPermissions()?.globalPermissions || [];
if (
PermissionUtil.getProjectPermissions() &&
PermissionUtil.getProjectPermissions()?.permissions &&
PermissionUtil.getProjectPermissions()!.permissions.length > 0
) {
userPermissions = userPermissions.concat(
PermissionUtil.getProjectPermissions()!.permissions.map(
(i: UserPermission) => {
return i.permission;
},
),
);
}
userPermissions.push(Permission.Public);
return userPermissions;
};
useEffect(() => {
serializeToTableColumns();
}, []);
useEffect(() => {
serializeToTableColumns();
}, [data]);
const setActionSchema: VoidFunction = () => {
const permissions: Array<Permission> = PermissionUtil.getAllPermissions();
const actionsSchema: Array<ActionButtonSchema<TBaseModel>> = [];
if (props.showViewIdButton) {
actionsSchema.push({
title: "Show ID",
buttonStyleType: ButtonStyleType.OUTLINE,
hideOnMobile: true,
onClick: async (
item: TBaseModel,
onCompleteAction: VoidFunction,
onError: ErrorFunction,
) => {
try {
setViewId(item["_id"] as string);
setShowViewIdModal(true);
onCompleteAction();
} catch (err) {
onError(err as Error);
}
},
});
}
// add actions buttons from props.
if (props.actionButtons) {
for (const moreSchema of props.actionButtons) {
actionsSchema.push(moreSchema);
}
}
if (permissions) {
if (
props.isViewable &&
(model.hasReadPermissions(permissions) || User.isMasterAdmin())
) {
actionsSchema.push({
title:
props.viewButtonText ||
`View ${props.singularName || model.singularName}`,
buttonStyleType: ButtonStyleType.NORMAL,
onClick: async (
item: TBaseModel,
onCompleteAction: VoidFunction,
onError: ErrorFunction,
) => {
try {
let baseModel: TBaseModel = item;
if (
!(item instanceof BaseModel) &&
!(item instanceof AnalyticsBaseModel)
) {
baseModel = props.callbacks.getModelFromJSON(
item as JSONObject,
);
}
if (props.onBeforeView) {
item = await props.onBeforeView(baseModel);
}
if (props.onViewPage) {
const route: Route | URL = await props.onViewPage(baseModel);
onCompleteAction();
if (props.onViewComplete) {
props.onViewComplete(baseModel);
}
return Navigation.navigate(route);
}
if (!props.viewPageRoute) {
throw new BadDataException("props.viewPageRoute not found");
}
onCompleteAction();
if (props.onViewComplete) {
props.onViewComplete(baseModel);
}
const id: string = baseModel.id?.toString() || "";
return Navigation.navigate(
new Route(props.viewPageRoute.toString()).addRoute("/" + id),
);
} catch (err) {
onError(err as Error);
}
},
});
}
if (
props.isEditable &&
(model.hasUpdatePermissions(permissions) || User.isMasterAdmin())
) {
actionsSchema.push({
title: props.editButtonText || "Edit",
buttonStyleType: ButtonStyleType.OUTLINE,
onClick: async (
item: TBaseModel,
onCompleteAction: VoidFunction,
onError: ErrorFunction,
) => {
try {
if (props.onBeforeEdit) {
item = await props.onBeforeEdit(item);
}
setModalType(ModalType.Edit);
setShowModal(true);
setCurrentEditableItem(item);
onCompleteAction();
} catch (err) {
onError(err as Error);
}
},
});
}
if (
props.isDeleteable &&
(model.hasDeletePermissions(permissions) || User.isMasterAdmin())
) {
actionsSchema.push({
title: props.deleteButtonText || "Delete",
icon: IconProp.Trash,
buttonStyleType: ButtonStyleType.DANGER_OUTLINE,
onClick: async (
item: TBaseModel,
onCompleteAction: VoidFunction,
onError: ErrorFunction,
) => {
try {
if (props.onBeforeDelete) {
item = await props.onBeforeDelete(item);
}
setShowDeleteConfirmModal(true);
setCurrentDeleteableItem(item);
onCompleteAction();
} catch (err) {
onError(err as Error);
}
},
});
}
}
setActionButtonSchema(actionsSchema);
};
const [filterData, setFilterData] = useState<FilterData<TBaseModel>>(
props.initialFilterData || {},
);
type OnFilterChangedFunction = (filterData: FilterData<TBaseModel>) => void;
const onFilterChanged: OnFilterChangedFunction = (
filterData: FilterData<TBaseModel>,
): void => {
const newQuery: Query<TBaseModel> = {};
setFilterData(filterData);
for (const key in filterData) {
if (filterData[key] && typeof filterData[key] === Typeof.String) {
newQuery[key as keyof TBaseModel] = (filterData[key] || "").toString();
}
if (typeof filterData[key] === Typeof.Boolean) {
newQuery[key as keyof TBaseModel] = Boolean(filterData[key]);
}
if (typeof filterData[key] === Typeof.Number) {
newQuery[key as keyof TBaseModel] = filterData[key];
}
if (filterData[key] instanceof Date) {
newQuery[key as keyof TBaseModel] = filterData[key];
}
if (filterData[key] instanceof Search) {
newQuery[key as keyof TBaseModel] = filterData[key];
}
if (filterData[key] instanceof InBetween) {
newQuery[key as keyof TBaseModel] = filterData[key];
}
if (
props.filters.find((f: Filter<TBaseModel>) => {
return f.field && f.field[key];
})?.type === FieldType.JSON &&
typeof filterData[key] === Typeof.Object
) {
newQuery[key as keyof TBaseModel] = filterData[key];
}
if (Array.isArray(filterData[key])) {
newQuery[key as keyof TBaseModel] = new Includes(
filterData[key] as Array<string>,
);
}
}
setQuery({ ...newQuery });
};
type GetDeleteBulkActionFunction = () => BulkActionButtonSchema<TBaseModel>;
const getDeleteBulkAction: GetDeleteBulkActionFunction =
(): BulkActionButtonSchema<TBaseModel> => {
return {
title: "Delete",
buttonStyleType: ButtonStyleType.NORMAL,
icon: IconProp.Trash,
confirmMessage: (items: Array<TBaseModel>) => {
return `Are you sure you want to delete ${items.length} ${
props.pluralName || model.pluralName || "items"
}?`;
},
confirmTitle: (items: Array<TBaseModel>) => {
return `Delete ${items.length} ${
props.pluralName || model.pluralName || "items"
}`;
},
confirmButtonStyleType: ButtonStyleType.DANGER,
onClick: async ({
items,
onProgressInfo,
onBulkActionStart,
onBulkActionEnd,
}: BulkActionOnClickProps<TBaseModel>) => {
onBulkActionStart();
const inProgressItems: Array<TBaseModel> = [...items];
const successItems: Array<TBaseModel> = [];
const failedItems: Array<BulkActionFailed<TBaseModel>> = [];
for (let i: number = 0; i < items.length; i++) {
try {
const item: TBaseModel = items[i]!;
// remove items from inProgressItems
inProgressItems.splice(inProgressItems.indexOf(item), 1);
await props.callbacks.deleteItem(item);
successItems.push(item);
onProgressInfo({
inProgressItems: inProgressItems,
successItems: successItems,
failed: failedItems,
totalItems: items,
});
} catch (err) {
failedItems.push({
item: items[i]!,
failedMessage: API.getFriendlyMessage(err),
});
}
}
onBulkActionEnd();
},
};
};
const getTable: GetReactElementFunction = (): ReactElement => {
return (
<Table
onFilterChanged={(filterData: FilterData<TBaseModel>) => {
onFilterChanged(filterData);
setTableView(null);
// check if there's anything in the filter data and update the isFilterApplied prop.
let isFilterApplied: boolean = false;
for (const key in filterData) {
if (filterData[key]) {
isFilterApplied = true;
break;
}
}
if (props.onFilterApplied) {
props.onFilterApplied(isFilterApplied);
}
}}
filterData={filterData}
className={
props.cardProps
? ""
: "rounded-lg border-2 border-gray-200 p-6 pt-0 pb-5"
}
tableContainerClassName={
props.cardProps ? "" : "overflow-hidden rounded"
}
onFilterRefreshClick={async () => {
await getFilterDropdownItems();
}}
bulkActions={{
buttons: props.bulkActions?.buttons.map(
(
action:
| BulkActionButtonSchema<TBaseModel>
| ModalTableBulkDefaultActions,
) => {
const permissions: Array<Permission> =
PermissionUtil.getAllPermissions();
if (
action === ModalTableBulkDefaultActions.Delete &&
model.hasDeletePermissions(permissions)
) {
return getDeleteBulkAction();
}
return action;
},
) as Array<BulkActionButtonSchema<TBaseModel>>,
}}
onBulkActionEnd={async () => {
setBulkSelectedItems([]);
await fetchItems();
}}
onBulkActionStart={() => {}}
bulkSelectedItems={bulkSelectedItems}
onBulkSelectedItemAdded={(item: TBaseModel) => {
setBulkSelectedItems([...bulkSelectedItems, item]);
}}
onBulkSelectedItemRemoved={(item: TBaseModel) => {
setBulkSelectedItems(
bulkSelectedItems.filter((i: TBaseModel) => {
return (
i[matchBulkSelectedItemByField]?.toString() !==
item[matchBulkSelectedItemByField]?.toString()
);
}),
);
}}
sortBy={sortBy}
sortOrder={sortOrder}
onBulkSelectItemsOnCurrentPage={() => {
const items: TBaseModel[] = [...bulkSelectedItems, ...data];
// remove duplicates
const uniqueItems: TBaseModel[] = items.filter(
(item: TBaseModel, index: number, self: Array<TBaseModel>) => {
return (
index ===
self.findIndex((t: TBaseModel) => {
return (
t[matchBulkSelectedItemByField]?.toString() ===
item[matchBulkSelectedItemByField]?.toString()
);
})
);
},
);
setBulkSelectedItems(uniqueItems);
}}
onBulkClearAllItems={() => {
setBulkSelectedItems([]);
}}
onBulkSelectAllItems={async () => {
await fetchAllBulkItems();
}}
matchBulkSelectedItemByField={matchBulkSelectedItemByField || "_id"}
bulkItemToString={(item: TBaseModel) => {
return (
(props.singularName || item.singularName || "") +
" " +
item[matchBulkSelectedItemByField]?.toString() +
" " || ""
);
}}
filters={classicTableFilters}
filterError={tableFilterError}
isFilterLoading={isFilterFetchLoading}
showFilterModal={showFilterModal}
onFilterModalClose={() => {
setShowFilterModal(false);
}}
onFilterModalOpen={() => {
setShowFilterModal(true);
}}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
onSortChanged={(
sortBy: keyof TBaseModel | null,
sortOrder: SortOrder,
) => {
setSortBy(sortBy);
setSortOrder(sortOrder);
setTableView(null);
}}
singularLabel={props.singularName || model.singularName || "Item"}
pluralLabel={props.pluralName || model.pluralName || "Items"}
error={error}
currentPageNumber={currentPageNumber}
isLoading={isLoading}
enableDragAndDrop={props.enableDragAndDrop}
dragDropIdField={"_id"}
dragDropIndexField={props.dragDropIndexField}
totalItemsCount={totalItemsCount}
data={data}
id={props.id}
columns={tableColumns}
itemsOnPage={itemsOnPage}
onDragDrop={async (id: string, newOrder: number) => {
if (!props.dragDropIndexField) {
return;
}
setIsLoading(true);
await props.callbacks.updateById({
id: new ObjectID(id),
data: {
[props.dragDropIndexField]: newOrder,
},
});
await fetchItems();
}}
disablePagination={props.disablePagination || false}
onNavigateToPage={async (
pageNumber: number,
newItemsOnPage: number,
) => {
setCurrentPageNumber(pageNumber);
if (newItemsOnPage !== itemsOnPage) {
setTableView(null);
}
setItemsOnPage(newItemsOnPage);
}}
noItemsMessage={props.noItemsMessage || ""}
onRefreshClick={async () => {
await fetchItems();
}}
actionButtons={actionButtonSchema}
/>
);
};
const getOrderedStatesList: GetReactElementFunction = (): ReactElement => {
if (!props.orderedStatesListProps) {
throw new BadDataException(
"props.orderedStatesListProps required when showAs === ShowAs.OrderedStatesList",
);
}
let getTitleElement:
| ((
item: TBaseModel,
onBeforeFetchData?: TBaseModel | undefined,
) => ReactElement)
| undefined = undefined;
let getDescriptionElement:
| ((item: TBaseModel) => ReactElement)
| undefined = undefined;
for (const column of props.columns) {
const key: string | undefined = Object.keys(
column.field as SelectEntityField<TBaseModel>,
)[0];
if (key === props.orderedStatesListProps.titleField) {
getTitleElement = column.getElement;
}
if (key === props.orderedStatesListProps.descriptionField) {
getDescriptionElement = column.getElement;
}
}
return (
<OrderedStatesList<TBaseModel>
error={error}
isLoading={isLoading}
data={data}
id={props.id}
titleField={props.orderedStatesListProps?.titleField}
descriptionField={props.orderedStatesListProps?.descriptionField}
orderField={props.orderedStatesListProps?.orderField}
shouldAddItemInTheBeginning={
props.orderedStatesListProps.shouldAddItemInTheBeginning
}
shouldAddItemInTheEnd={
props.orderedStatesListProps.shouldAddItemInTheEnd
}
noItemsMessage={props.noItemsMessage || ""}
onRefreshClick={async () => {
await fetchItems();
}}
onCreateNewItem={
props.isCreateable
? (order: number) => {
setOrderedStatesListNewItemOrder(order);
setModalType(ModalType.Create);
setShowModal(true);
}
: undefined
}
singularLabel={props.singularName || model.singularName || "Item"}
actionButtons={actionButtonSchema}
getTitleElement={getTitleElement}
getDescriptionElement={getDescriptionElement}
/>
);
};
const getList: GetReactElementFunction = (): ReactElement => {
return (
<List
onFilterChanged={(filterData: FilterData<TBaseModel>) => {
onFilterChanged(filterData);
}}
onFilterRefreshClick={async () => {
await getFilterDropdownItems();
}}
filters={classicTableFilters}
filterError={tableFilterError}
isFilterLoading={isFilterFetchLoading}
showFilterModal={showFilterModal}
onFilterModalClose={() => {
setShowFilterModal(false);
}}
onFilterModalOpen={() => {
setShowFilterModal(true);
}}
onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
singularLabel={props.singularName || model.singularName || "Item"}
pluralLabel={props.pluralName || model.pluralName || "Items"}
error={error}
currentPageNumber={currentPageNumber}
listDetailOptions={props.listDetailOptions}
enableDragAndDrop={props.enableDragAndDrop}
onDragDrop={async (id: string, newOrder: number) => {
if (!props.dragDropIndexField) {
return;
}
setIsLoading(true);
await props.callbacks.updateById({
id: new ObjectID(id),
data: {
[props.dragDropIndexField]: newOrder,
},
});
await fetchItems();
}}
dragDropIdField={"_id"}
dragDropIndexField={props.dragDropIndexField}
isLoading={isLoading}
totalItemsCount={totalItemsCount}
data={data}
id={props.id}
fields={fields}
itemsOnPage={itemsOnPage}
disablePagination={props.disablePagination || false}
onNavigateToPage={async (pageNumber: number, itemsOnPage: number) => {
setCurrentPageNumber(pageNumber);
setItemsOnPage(itemsOnPage);
}}
noItemsMessage={props.noItemsMessage || ""}
onRefreshClick={async () => {
await fetchItems();
}}
actionButtons={actionButtonSchema}
/>
);
};
type GetCardTitleFunction = (title: ReactElement | string) => ReactElement;
const getCardTitle: GetCardTitleFunction = (
title: ReactElement | string,
): ReactElement => {
const plan: PlanType | null = ProjectUtil.getCurrentPlan();
let showPlan: boolean = Boolean(
BILLING_ENABLED &&
plan &&
new props.modelType().getReadBillingPlan() &&
!SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(
new props.modelType().getReadBillingPlan()!,
plan,
getAllEnvVars(),
),
);
let planName: string = new props.modelType().getReadBillingPlan()!;
if (props.isCreateable && !showPlan) {
// if createable then read create billing permissions.
showPlan = Boolean(
BILLING_ENABLED &&
plan &&
new props.modelType().getCreateBillingPlan() &&
!SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(
new props.modelType().getCreateBillingPlan()!,
plan,
getAllEnvVars(),
),
);
planName = new props.modelType().getCreateBillingPlan()!;
}
return (
<span>
{title}
{showPlan && (
<span
style={{
marginLeft: "5px",
}}
>
<Pill text={`${planName} Plan`} color={Yellow} />
</span>
)}
</span>
);
};
const getCardComponent: GetReactElementFunction = (): ReactElement => {
if (showAs === ShowAs.Table || showAs === ShowAs.List) {
return (
<div>
{props.cardProps && (
<Card
{...props.cardProps}
buttons={cardButtons}
bodyClassName={
showAs === ShowAs.List
? "-ml-6 -mr-6 bg-gray-50 border-top"
: ""
}
title={getCardTitle(props.cardProps.title || "")}
>
{tableColumns.length === 0 && props.columns.length > 0 ? (
<ErrorMessage
message={`You are not authorized to view this table. You need any one of these permissions: ${PermissionHelper.getPermissionTitles(
model.getReadPermissions(),
).join(", ")}`}
/>
) : (
<></>
)}
{tableColumns.length > 0 && showAs === ShowAs.Table ? (
getTable()
) : (
<></>
)}
{tableColumns.length > 0 && showAs === ShowAs.List ? (
getList()
) : (
<></>
)}
</Card>
)}
{!props.cardProps && showAs === ShowAs.Table ? getTable() : <></>}
{!props.cardProps && showAs === ShowAs.List ? getList() : <></>}
</div>
);
}
return (
<div>
{props.cardProps && (
<Card
{...props.cardProps}
buttons={cardButtons}
title={getCardTitle(props.cardProps.title || "")}
>
{getOrderedStatesList()}
</Card>
)}
{!props.cardProps && getOrderedStatesList()}
</div>
);
};
return (
<>
<div className="">{getCardComponent()}</div>
{showModel ? (
props.callbacks.showCreateEditModal({
onClose: () => {
setShowModal(false);
},
modalType: modalType,
onBeforeCreate: async (
item: TBaseModel,
miscDataProps: JSONObject,
) => {
if (
showAs === ShowAs.OrderedStatesList &&
props.orderedStatesListProps?.orderField &&
orderedStatesListNewItemOrder
) {
item.setColumnValue(
props.orderedStatesListProps.orderField as string,
orderedStatesListNewItemOrder,
);
}
if (props.onBeforeCreate) {
item = await props.onBeforeCreate(item, miscDataProps);
}
return item;
},
onSuccess: async (item: TBaseModel): Promise<void> => {
setShowModal(false);
setCurrentPageNumber(1);
await fetchItems();
if (props.onCreateSuccess) {
await props.onCreateSuccess(item);
}
return Promise.resolve();
},
modelIdToEdit: currentEditableItem
? new ObjectID(currentEditableItem["_id"] as string)
: undefined,
})
) : (
<></>
)}
{showDeleteConfirmModal && (
<ConfirmModal
title={`Delete ${props.singularName || model.singularName}`}
description={`Are you sure you want to delete this ${(
props.singularName ||
model.singularName ||
"item"
)?.toLowerCase()}?`}
onClose={() => {
setShowDeleteConfirmModal(false);
}}
submitButtonText={"Delete"}
onSubmit={async () => {
if (currentDeleteableItem && currentDeleteableItem["_id"]) {
await deleteItem(currentDeleteableItem);
setShowDeleteConfirmModal(false);
}
}}
submitButtonType={ButtonStyleType.DANGER}
/>
)}
{errorModalText && (
<ConfirmModal
title={`Error`}
description={`${errorModalText}`}
submitButtonText={"Close"}
onSubmit={() => {
setErrorModalText("");
}}
submitButtonType={ButtonStyleType.NORMAL}
/>
)}
{showViewIdModal && (
<ConfirmModal
title={`${props.singularName || model.singularName || ""} ID`}
description={
<div>
<span>
ID of this {props.singularName || model.singularName || ""}:{" "}
{viewId}
</span>
<br />
<br />
<span>
You can use this ID to interact with{" "}
{props.singularName || model.singularName || ""} via the
OneUptime API. Click the button below to go to API Reference.
</span>
</div>
}
onClose={() => {
setShowViewIdModal(false);
}}
submitButtonText={"Go to API Docs"}
onSubmit={() => {
setShowViewIdModal(false);
Navigation.navigate(
URL.fromString(API_DOCS_URL.toString()).addRoute(
"/" + model.getAPIDocumentationPath(),
),
{ openInNewTab: true },
);
}}
submitButtonType={ButtonStyleType.NORMAL}
closeButtonType={ButtonStyleType.OUTLINE}
/>
)}
{showHelpModal && props.helpContent && (
<Modal
title={props.helpContent.title}
description={props.helpContent.description}
onClose={() => {
setShowHelpModal(false);
}}
modalWidth={ModalWidth.Large}
submitButtonText="Close"
onSubmit={() => {
setShowHelpModal(false);
}}
>
<div className="p-2">
<MarkdownViewer text={props.helpContent.markdown} />
</div>
</Modal>
)}
</>
);
};
export default BaseModelTable;