From 9b983105945e5b5a87389a5d9a7d349e582f62ce Mon Sep 17 00:00:00 2001 From: Ayobami Akingbade Date: Wed, 3 Jan 2024 00:57:09 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(data):=20allow=20creating=20ne?= =?UTF-8?q?w=20data=20on=20empty=20views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/FEPaginationTable/index.tsx | 7 +++-- .../components/EmptyWrapper/Stories.tsx | 19 ++++++------ .../components/EmptyWrapper/index.tsx | 26 ++++++++++------- .../components/EmptyWrapper/types.ts | 4 +++ .../components/ListManager/index.tsx | 19 ++---------- .../design-system/components/Table/Body.tsx | 12 +++----- .../components/Table/Stories.tsx | 3 ++ .../components/Table/Table.spec.tsx | 3 ++ .../design-system/components/Table/index.tsx | 4 +-- .../design-system/components/Table/types.ts | 3 +- src/frontend/views/Dashboard/Skeleton.tsx | 2 +- .../Dashboard/Widget/_render/Table/index.tsx | 1 + .../views/data/Details/RelationsTable.tsx | 11 +++++-- .../views/data/Table/DataTable/index.tsx | 29 ++++++++++--------- .../_WholeEntityTable/EntityDataTable.tsx | 20 +++++++++++-- .../data/Table/_WholeEntityTable/index.tsx | 25 +++++++++------- .../views/data/Table/hooks/useTableState.ts | 4 +-- src/frontend/views/data/Table/index.tsx | 7 +++-- src/frontend/views/data/Table/types.ts | 2 +- .../views/data/Table/useTableMenuItems.ts | 21 ++++++++++---- src/frontend/views/entity/Actions/Base.tsx | 8 ++++- src/frontend/views/entity/Views/Form.tsx | 1 + src/frontend/views/roles/List.tsx | 8 ++++- .../Variables/ManageCredentialGroup.tsx | 27 ++++++++++------- src/frontend/views/users/List.tsx | 8 ++++- 25 files changed, 172 insertions(+), 102 deletions(-) create mode 100644 src/frontend/design-system/components/EmptyWrapper/types.ts diff --git a/src/frontend/components/FEPaginationTable/index.tsx b/src/frontend/components/FEPaginationTable/index.tsx index d71659879..32b63b7d7 100644 --- a/src/frontend/components/FEPaginationTable/index.tsx +++ b/src/frontend/components/FEPaginationTable/index.tsx @@ -5,6 +5,7 @@ import { TableSkeleton } from "frontend/design-system/components/Skeleton/Table" import { Table } from "frontend/design-system/components/Table"; import { ITableColumn } from "frontend/design-system/components/Table/types"; import { TableFilterType } from "frontend/design-system/components/Table/filters/types"; +import { IEmptyWrapperProps } from "frontend/design-system/components/EmptyWrapper/types"; import { useFEPagination } from "./useFEPagination"; import { ViewStateMachine } from "../ViewStateMachine"; @@ -26,14 +27,14 @@ export interface IFETableColumn> { interface IProps> { columns: IFETableColumn[]; dataEndpoint: string; - emptyMessage: string; + empty: IEmptyWrapperProps; border?: true; } export function FEPaginationTable>({ columns, dataEndpoint, - emptyMessage, + empty, border, }: IProps) { const [paginatedDataState, setPaginatedDataState] = @@ -59,7 +60,7 @@ export function FEPaginationTable>({ overridePaginatedDataState: DEFAULT_TABLE_STATE, }} border={border} - emptyMessage={emptyMessage} + empty={empty} columns={columns as ITableColumn[]} /> diff --git a/src/frontend/design-system/components/EmptyWrapper/Stories.tsx b/src/frontend/design-system/components/EmptyWrapper/Stories.tsx index 48b38297c..8908e6342 100644 --- a/src/frontend/design-system/components/EmptyWrapper/Stories.tsx +++ b/src/frontend/design-system/components/EmptyWrapper/Stories.tsx @@ -2,7 +2,9 @@ import React from "react"; import { Story } from "@storybook/react"; import { ApplicationRoot } from "frontend/components/ApplicationRoot"; -import { EmptyWrapper, IProps } from "."; +import { noop } from "shared/lib/noop"; +import { EmptyWrapper } from "."; +import { IEmptyWrapperProps } from "./types"; export default { title: "Components/EmptyWrapper", @@ -12,7 +14,7 @@ export default { }, }; -const Template: Story = (args) => ( +const Template: Story = (args) => ( @@ -21,11 +23,10 @@ const Template: Story = (args) => ( export const Default = Template.bind({}); Default.args = {}; -export const WithChildren = Template.bind({}); -WithChildren.args = { - children: ( - <> - This is bold This is not - - ), +export const WithCreateNew = Template.bind({}); +WithCreateNew.args = { + createNew: { + label: "Add New Item", + action: () => noop(), + }, }; diff --git a/src/frontend/design-system/components/EmptyWrapper/index.tsx b/src/frontend/design-system/components/EmptyWrapper/index.tsx index 676429eb1..14c22d061 100644 --- a/src/frontend/design-system/components/EmptyWrapper/index.tsx +++ b/src/frontend/design-system/components/EmptyWrapper/index.tsx @@ -1,13 +1,11 @@ -import React, { ReactNode } from "react"; +import React from "react"; import styled from "styled-components"; import { Frown as Droplet } from "react-feather"; import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; import { Typo } from "frontend/design-system/primitives/Typo"; - -export interface IProps { - text: string; - children?: ReactNode; -} +import { Spacer } from "frontend/design-system/primitives/Spacer"; +import { SoftButton } from "../Button/SoftButton"; +import { IEmptyWrapperProps } from "./types"; const Root = styled.div` text-align: center; @@ -17,14 +15,22 @@ const Root = styled.div` background: ${USE_ROOT_COLOR("base-color")}; `; -export function EmptyWrapper({ text, children }: IProps) { +export function EmptyWrapper({ text, createNew }: IEmptyWrapperProps) { return ( -
-
+ {text} - {children} + {createNew && ( + <> + + + + )}
); } diff --git a/src/frontend/design-system/components/EmptyWrapper/types.ts b/src/frontend/design-system/components/EmptyWrapper/types.ts new file mode 100644 index 000000000..10221a519 --- /dev/null +++ b/src/frontend/design-system/components/EmptyWrapper/types.ts @@ -0,0 +1,4 @@ +export interface IEmptyWrapperProps { + text: string; + createNew?: { action: string | (() => void); label: string }; +} diff --git a/src/frontend/design-system/components/ListManager/index.tsx b/src/frontend/design-system/components/ListManager/index.tsx index 580659e99..96847d085 100644 --- a/src/frontend/design-system/components/ListManager/index.tsx +++ b/src/frontend/design-system/components/ListManager/index.tsx @@ -1,13 +1,12 @@ import React, { ReactNode, useState } from "react"; -import { Spacer } from "frontend/design-system/primitives/Spacer"; import styled from "styled-components"; import { DataStateKeys } from "frontend/lib/data/types"; import { ViewStateMachine } from "frontend/components/ViewStateMachine"; import { EmptyWrapper } from "../EmptyWrapper"; import { FormSearch } from "../Form/FormSearch"; -import { SoftButton } from "../Button/SoftButton"; import { defaultSearchFunction } from "./utils"; import { ListSkeleton } from "../Skeleton/List"; +import { IEmptyWrapperProps } from "../EmptyWrapper/types"; export { ListManagerItem } from "./ListManagerItem"; @@ -31,10 +30,7 @@ export interface IProps> { items: DataStateKeys; listLengthGuess: number; labelField: K; - empty?: { - text: string; - createNew?: { action: string | (() => void); label: string }; - }; + empty?: IEmptyWrapperProps; getLabel?: (name: string) => string; render: (item: T & { label: string }, index: number) => ReactNode; } @@ -71,16 +67,7 @@ export function ListManager>({ loader={} > {itemsLength === 0 ? ( - - - {empty?.createNew && ( - - )} - + ) : ( {itemsLength > SEARCH_THRESHOLD ? ( diff --git a/src/frontend/design-system/components/Table/Body.tsx b/src/frontend/design-system/components/Table/Body.tsx index 2ceb98b35..665a66cf4 100644 --- a/src/frontend/design-system/components/Table/Body.tsx +++ b/src/frontend/design-system/components/Table/Body.tsx @@ -5,6 +5,7 @@ import { USE_ROOT_COLOR } from "frontend/design-system/theme/root"; import { useThemeColorShade } from "frontend/design-system/theme/useTheme"; import { Typo } from "frontend/design-system/primitives/Typo"; import { EmptyWrapper } from "../EmptyWrapper"; +import { IEmptyWrapperProps } from "../EmptyWrapper/types"; const Td = styled.td` padding: 0.45rem; @@ -29,15 +30,10 @@ interface IProps { table: Table>; dataLength: number; isLoading: boolean; - emptyMessage?: string; + empty: IEmptyWrapperProps; } -export function TableBody({ - table, - dataLength, - emptyMessage, - isLoading, -}: IProps) { +export function TableBody({ table, dataLength, empty, isLoading }: IProps) { const colorShade = useThemeColorShade(); return ( @@ -58,7 +54,7 @@ export function TableBody({ {isLoading ? (
) : ( - + )} diff --git a/src/frontend/design-system/components/Table/Stories.tsx b/src/frontend/design-system/components/Table/Stories.tsx index f8b7bf447..4c89458a1 100644 --- a/src/frontend/design-system/components/Table/Stories.tsx +++ b/src/frontend/design-system/components/Table/Stories.tsx @@ -28,6 +28,9 @@ export default { }, syncPaginatedDataStateOut: action("setPaginatedDataState"), columns: TABLE_COLUMNS, + empty: { + text: "Empty Table", + }, tableData: TABLE_DATA, } as ITableProps, }; diff --git a/src/frontend/design-system/components/Table/Table.spec.tsx b/src/frontend/design-system/components/Table/Table.spec.tsx index e853cd3d0..e4371191d 100644 --- a/src/frontend/design-system/components/Table/Table.spec.tsx +++ b/src/frontend/design-system/components/Table/Table.spec.tsx @@ -12,6 +12,9 @@ const DEFAULT_TABLE_PROPS: ITableProps = { }, syncPaginatedDataStateOut: jest.fn(), columns: TABLE_COLUMNS, + empty: { + text: "Empty Table", + }, tableData: TABLE_DATA, }; diff --git a/src/frontend/design-system/components/Table/index.tsx b/src/frontend/design-system/components/Table/index.tsx index 501c6cef8..bfab8ea04 100644 --- a/src/frontend/design-system/components/Table/index.tsx +++ b/src/frontend/design-system/components/Table/index.tsx @@ -56,7 +56,7 @@ export function Table({ columns, lean, border, - emptyMessage, + empty, }: ITableProps) { const { data = { @@ -138,7 +138,7 @@ export function Table({ diff --git a/src/frontend/design-system/components/Table/types.ts b/src/frontend/design-system/components/Table/types.ts index c1bb4116e..454dbc6f7 100644 --- a/src/frontend/design-system/components/Table/types.ts +++ b/src/frontend/design-system/components/Table/types.ts @@ -3,6 +3,7 @@ import { ReactNode } from "react"; import { UseQueryResult } from "react-query"; import { IPaginatedDataState, PaginatedData } from "shared/types/data"; import { TableFilterType } from "./filters/types"; +import { IEmptyWrapperProps } from "../EmptyWrapper/types"; export interface ITableColumn { Header: @@ -29,5 +30,5 @@ export interface ITableProps { border?: boolean; overridePaginatedDataState?: IPaginatedDataState; syncPaginatedDataStateOut: (params: IPaginatedDataState) => void; - emptyMessage?: string; + empty: IEmptyWrapperProps; } diff --git a/src/frontend/views/Dashboard/Skeleton.tsx b/src/frontend/views/Dashboard/Skeleton.tsx index dbbfb0a46..da5a2c07d 100644 --- a/src/frontend/views/Dashboard/Skeleton.tsx +++ b/src/frontend/views/Dashboard/Skeleton.tsx @@ -22,7 +22,7 @@ export function DashboardSkeleton() { - + diff --git a/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx b/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx index d0241d03b..bdb5474e6 100644 --- a/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx +++ b/src/frontend/views/Dashboard/Widget/_render/Table/index.tsx @@ -30,6 +30,7 @@ export function TableWidget({ data }: IProps) { }} syncPaginatedDataStateOut={() => {}} border + empty={{ text: "No Data" }} lean columns={columns} /> diff --git a/src/frontend/views/data/Details/RelationsTable.tsx b/src/frontend/views/data/Details/RelationsTable.tsx index 051a4d05e..9652dbe67 100644 --- a/src/frontend/views/data/Details/RelationsTable.tsx +++ b/src/frontend/views/data/Details/RelationsTable.tsx @@ -12,7 +12,10 @@ import { } from "frontend/hooks/entity/entity.config"; import { ENTITY_DETAILS_VIEW_KEY } from "./constants"; import { DetailsLayout } from "./_Layout"; -import { useTableMenuItems } from "../Table/useTableMenuItems"; +import { + getEntityCreateLink, + useTableMenuItems, +} from "../Table/useTableMenuItems"; import { WholeEntityTable } from "../Table/_WholeEntityTable"; export function EntityRelationTable() { @@ -55,7 +58,11 @@ export function EntityRelationTable() { 0 + empty={ + currentState.filters.length > 0 && + persistentFilters.length !== currentState.filters.length ? // TODO: for contributors: transform this to user readable message - `No result for the current ${pluralize({ - singular: "filter", - count: currentState.filters.length, - inclusive: true, - })} applied.` - : crudConfig.TEXT_LANG.EMPTY_LIST + { + text: `No result for the current ${pluralize({ + singular: "filter", + count: currentState.filters.length, + inclusive: true, + })} applied.`, + } + : empty } columns={columns.filter( (column) => !skipColumns.includes(column.accessor) diff --git a/src/frontend/views/data/Table/_WholeEntityTable/EntityDataTable.tsx b/src/frontend/views/data/Table/_WholeEntityTable/EntityDataTable.tsx index 2c78445cf..bd72d91e9 100644 --- a/src/frontend/views/data/Table/_WholeEntityTable/EntityDataTable.tsx +++ b/src/frontend/views/data/Table/_WholeEntityTable/EntityDataTable.tsx @@ -6,14 +6,22 @@ import { useTableColumns } from "../useTableColumns"; import { TableViewComponent } from "../portal"; import { IDataTableProps } from "../types"; import { BaseDataTable } from "../DataTable"; +import { useCanUserPerformCrudAction } from "../../hooks/useCanUserPerformCrudAction"; interface IProps extends IDataTableProps { entity: string; + createNewLink: string; tabKey?: string; } -export function EntityDataTable({ entity, tabKey = "", ...props }: IProps) { +export function EntityDataTable({ + entity, + createNewLink, + tabKey = "", + ...props +}: IProps) { const columns = useTableColumns(entity); + const canUserPerformCrudAction = useCanUserPerformCrudAction(entity); const entityCrudConfig = useEntityCrudConfig(entity); @@ -33,7 +41,15 @@ export function EntityDataTable({ entity, tabKey = "", ...props }: IProps) { dataEndpoint={ENTITY_TABLE_PATH(entity)} columns={columns.data || []} stateStorageKey={`${entity}${tabKey}`} - crudConfig={entityCrudConfig} + empty={{ + text: entityCrudConfig.TEXT_LANG.EMPTY_LIST, + createNew: canUserPerformCrudAction("create") + ? { + label: entityCrudConfig.TEXT_LANG.CREATE, + action: createNewLink, + } + : undefined, + }} {...props} /> )} diff --git a/src/frontend/views/data/Table/_WholeEntityTable/index.tsx b/src/frontend/views/data/Table/_WholeEntityTable/index.tsx index 823af0833..619213cc6 100644 --- a/src/frontend/views/data/Table/_WholeEntityTable/index.tsx +++ b/src/frontend/views/data/Table/_WholeEntityTable/index.tsx @@ -14,14 +14,16 @@ import { EntityDataTable } from "./EntityDataTable"; interface IProps { entity: string; - persistFilters?: FieldQueryFilter[]; + persistentFilters?: FieldQueryFilter[]; skipColumns?: string[]; + createNewLink: string; } export function WholeEntityTable({ entity, - persistFilters = [], + persistentFilters = [], skipColumns, + createNewLink, }: IProps) { const tabFromUrl = useRouteParam("tab"); const changeTabParam = useChangeRouterParam("tab"); @@ -38,11 +40,18 @@ export function WholeEntityTable({ id, filters: [ ...(dataState.filters as FieldQueryFilter[]), - ...persistFilters, + ...persistentFilters, ], })) ); + const dataTableProps = { + entity, + persistentFilters, + skipColumns, + createNewLink, + }; + return ( <> @@ -68,11 +77,9 @@ export function WholeEntityTable({ return { content: ( ), label: title.trim(), @@ -81,11 +88,7 @@ export function WholeEntityTable({ })} /> ) : ( - + )} diff --git a/src/frontend/views/data/Table/hooks/useTableState.ts b/src/frontend/views/data/Table/hooks/useTableState.ts index 57803d727..5e91af8d5 100644 --- a/src/frontend/views/data/Table/hooks/useTableState.ts +++ b/src/frontend/views/data/Table/hooks/useTableState.ts @@ -8,7 +8,7 @@ import { useEntityContextState } from "./useEntityContextState"; export const useTableState = ( contextKey: string, - persitentFilters: IDataTableProps["persitentFilters"], + persistentFilters: IDataTableProps["persistentFilters"], defaultTableState?: IDataTableProps["defaultTableState"] ) => { /* @@ -42,7 +42,7 @@ export const useTableState = ( */ const currentState: IPaginatedDataState = { ...paginatedDataState, - filters: [...paginatedDataState.filters, ...persitentFilters], + filters: [...paginatedDataState.filters, ...persistentFilters], }; /* diff --git a/src/frontend/views/data/Table/index.tsx b/src/frontend/views/data/Table/index.tsx index 43afcf7f6..f3f4bfea8 100644 --- a/src/frontend/views/data/Table/index.tsx +++ b/src/frontend/views/data/Table/index.tsx @@ -10,7 +10,7 @@ import { EntityActionTypes, useEntityActionMenuItems, } from "../../entity/constants"; -import { useTableMenuItems } from "./useTableMenuItems"; +import { getEntityCreateLink, useTableMenuItems } from "./useTableMenuItems"; import { WholeEntityTable } from "./_WholeEntityTable"; export function EntityTable() { @@ -40,7 +40,10 @@ export function EntityTable() { return ( - + ); } diff --git a/src/frontend/views/data/Table/types.ts b/src/frontend/views/data/Table/types.ts index 031a1d269..64bf0825c 100644 --- a/src/frontend/views/data/Table/types.ts +++ b/src/frontend/views/data/Table/types.ts @@ -1,7 +1,7 @@ import { FieldQueryFilter, IPaginatedDataState } from "shared/types/data"; export interface IDataTableProps { - persitentFilters?: FieldQueryFilter[]; + persistentFilters?: FieldQueryFilter[]; skipColumns?: string[]; defaultTableState?: Pick< IPaginatedDataState, diff --git a/src/frontend/views/data/Table/useTableMenuItems.ts b/src/frontend/views/data/Table/useTableMenuItems.ts index b0bd74bfe..6d644ed4c 100644 --- a/src/frontend/views/data/Table/useTableMenuItems.ts +++ b/src/frontend/views/data/Table/useTableMenuItems.ts @@ -6,6 +6,21 @@ import { IDropDownMenuItem } from "frontend/design-system/components/DropdownMen import { usePluginTableMenuItems } from "./portal"; import { useCanUserPerformCrudAction } from "../hooks/useCanUserPerformCrudAction"; +export const getEntityCreateLink = ( + entity: string, + reference?: { + referenceField: string; + entityId: string; + } +) => { + let baseUrl = NAVIGATION_LINKS.ENTITY.CREATE(entity); + if (reference) { + baseUrl = `${baseUrl}?${reference.referenceField}=${reference.entityId}`; + } + + return baseUrl; +}; + export const useTableMenuItems = ( entity: string, reference?: { @@ -34,11 +49,7 @@ export const useTableMenuItems = ( label: entityCrudConfig.TEXT_LANG.CREATE, IconComponent: Plus, onClick: () => { - let baseUrl = NAVIGATION_LINKS.ENTITY.CREATE(entity); - if (reference) { - baseUrl = `${baseUrl}?${reference.referenceField}=${reference.entityId}`; - } - router.push(baseUrl); + router.push(getEntityCreateLink(entity, reference)); }, }); } diff --git a/src/frontend/views/entity/Actions/Base.tsx b/src/frontend/views/entity/Actions/Base.tsx index 53db8a089..7304b8325 100644 --- a/src/frontend/views/entity/Actions/Base.tsx +++ b/src/frontend/views/entity/Actions/Base.tsx @@ -141,10 +141,16 @@ export function BaseActionInstances(actionInstanceView: ActionInstanceView) { setCurrentInstanceItem(NEW_ACTION_ITEM), + }, + }} /> )} diff --git a/src/frontend/views/roles/List.tsx b/src/frontend/views/roles/List.tsx index add54b80b..3d9a564d1 100644 --- a/src/frontend/views/roles/List.tsx +++ b/src/frontend/views/roles/List.tsx @@ -89,8 +89,14 @@ export function ListRoles() { diff --git a/src/frontend/views/settings/Variables/ManageCredentialGroup.tsx b/src/frontend/views/settings/Variables/ManageCredentialGroup.tsx index b2833441c..531d8bba4 100644 --- a/src/frontend/views/settings/Variables/ManageCredentialGroup.tsx +++ b/src/frontend/views/settings/Variables/ManageCredentialGroup.tsx @@ -76,6 +76,8 @@ export function ManageCredentialGroup({ setCurrentConfigItem(""); }; + const CRUD_CONFIG = INTEGRATIONS_GROUP_CONFIG[group].crudConfig; + const MemoizedAction = useCallback( ({ row }: IFETableCell) => ( @@ -124,8 +126,7 @@ export function ManageCredentialGroup({ setCurrentConfigItem(NEW_CONFIG_ITEM); }, IconComponent: Plus, - label: - INTEGRATIONS_GROUP_CONFIG[group].crudConfig.TEXT_LANG.CREATE, + label: CRUD_CONFIG.TEXT_LANG.CREATE, }, ] : [], @@ -134,9 +135,7 @@ export function ManageCredentialGroup({ id: "help", onClick: () => setIsDocOpen(true), IconComponent: HelpCircle, - label: DOCUMENTATION_LABEL.CONCEPT( - INTEGRATIONS_GROUP_CONFIG[group].crudConfig.TEXT_LANG.TITLE - ), + label: DOCUMENTATION_LABEL.CONCEPT(CRUD_CONFIG.TEXT_LANG.TITLE), }, ], }; @@ -200,17 +199,23 @@ export function ManageCredentialGroup({ setCurrentConfigItem(NEW_CONFIG_ITEM), + } + : undefined, + }} columns={tableColumns} /> diff --git a/src/frontend/views/users/List.tsx b/src/frontend/views/users/List.tsx index 3f38fb857..00401609f 100644 --- a/src/frontend/views/users/List.tsx +++ b/src/frontend/views/users/List.tsx @@ -130,7 +130,13 @@ export function ListUsers() {