Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {
Heading,
IconDomain,
IllustratedMessage,
Text,
typedList,
} from "@mittwald/flow-react-components";
import { type Domain } from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
const List = typedList<Domain>();

const emptyView = (
<IllustratedMessage>
<IconDomain />
<Heading>Keine Domains gefunden</Heading>
<Text>Füge neue Domains hinzu, um zu starten.</Text>
</IllustratedMessage>
);

return (
<List.List aria-label="Domains" emptyView={emptyView}>
<List.StaticData data={[]} />
<List.Item>{() => null}</List.Item>
</List.List>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { typedList } from "@mittwald/flow-react-components";
import { type Domain } from "@/content/04-components/structure/list/examples/domainApi";

export default () => {
const List = typedList<Domain>();

return (
<List.List aria-label="Domains">
<List.StaticData data={[]} />
<List.Item>{() => null}</List.Item>
</List.List>
);
};
13 changes: 13 additions & 0 deletions apps/docs/src/content/04-components/structure/list/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ Die Pagination ist standardmäßig bei jeder List aktiviert, kann jedoch über
`hidePagination` deaktiviert werden. Über die `batchSize`-Property kann
festgelegt werden, wie viele Einträge gleichzeitig angezeigt werden sollen.

## Empty View

Mit `emptyView` kann eine benutzerdefinierte Ansicht angezeigt werden, wenn die
Liste keine Einträge enthält. So lässt sich beispielsweise eine Illustration mit
einem Hinweistext anzeigen, um die Nutzer zu informieren, dass keine Daten
vorhanden sind, und ihnen gegebenenfalls Tipps zu geben, wie sie Daten
hinzufügen können.

In der Regel sollte als Empty View eine IllustratedMessage verwendet werden, um
eine konsistente Nutzererfahrung zu gewährleisten.

<LiveCodeEditor example="customEmptyView" editorCollapsed />

---

# Kombiniere mit ...
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/components/List/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { type PropsContext, PropsContextProvider } from "@/lib/propsContext";
import { deepFilterByType, deepFindOfType } from "@/lib/react/deepFindOfType";
import DivView from "@/views/DivView";
import { TunnelExit, TunnelProvider } from "@mittwald/react-tunnel";
import type { PropsWithChildren } from "react";
import type { PropsWithChildren, ReactNode } from "react";
import Footer from "./components/Footer";
import styles from "./List.module.css";
import { listContext } from "./listContext";
Expand All @@ -50,6 +50,8 @@ export interface ListProps<T, TMeta = unknown>
/** The number of items to be displayed on one page. */
batchSize?: number;
hidePagination?: boolean;
emptySearchResultView?: ReactNode;
emptyView?: ReactNode;
}

export const List = flowComponent("List", (props) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { FC } from "react";
import { IconSearch } from "@/components/Icon/components/icons";
import locales from "../../locales/*.locale.json";
import { useLocalizedStringFormatter } from "react-aria";
import IllustratedMessageView from "@/views/IllustratedMessageView";
import HeadingView from "@/views/HeadingView";
import TextView from "@/views/TextView";

export type EmptySearchResultViewProps = Record<string, never>;

export const EmptySearchResultView: FC<EmptySearchResultViewProps> = () => {
const stringFormatter = useLocalizedStringFormatter(locales);

return (
<IllustratedMessageView>
<IconSearch />
<HeadingView>
{stringFormatter.format("list.noResult.heading")}
</HeadingView>
<TextView>{stringFormatter.format("list.noResult.text")}</TextView>
</IllustratedMessageView>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./EmptySearchResultView";
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { FC } from "react";
import locales from "../../locales/*.locale.json";
import { useLocalizedStringFormatter } from "react-aria";
import IllustratedMessageView from "@/views/IllustratedMessageView";
import HeadingView from "@/views/HeadingView";
import TextView from "@/views/TextView";
import { IconClose } from "@/components/Icon/components/icons";

export type EmptyViewProps = Record<string, never>;

export const EmptyView: FC<EmptyViewProps> = () => {
const stringFormatter = useLocalizedStringFormatter(locales);

return (
<IllustratedMessageView>
<IconClose />
<HeadingView>
{stringFormatter.format("list.noItems.heading")}
</HeadingView>
<TextView>{stringFormatter.format("list.noItems.text")}</TextView>
</IllustratedMessageView>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./EmptyView";
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import DivView from "@/views/DivView";
import type { FC, ReactNode } from "react";
import styles from "../../components/Items/Items.module.scss";
import { EmptyView } from "../EmptyView/EmptyView";
import { EmptySearchResultView } from "../EmptySearchResultView";
import type { EmptyViewType } from "../../model/types";

export interface EmptyViewContainerProps {
emptyView?: ReactNode;
emptySearchResultView?: ReactNode;
viewType: EmptyViewType;
}

/** @flr-generate all */
export const EmptyViewContainer: FC<EmptyViewContainerProps> = (props) => {
const emptyView = props.emptyView ?? <EmptyView />;
const emptySearchResultView = props.emptySearchResultView ?? (
<EmptySearchResultView />
);
return (
<DivView className={styles.emptyView}>
{props.viewType === "search" ? emptySearchResultView : emptyView}
</DivView>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./view";
export * from "./EmptyViewContainer";
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* prettier-ignore */
/* This file is auto-generated with the remote-components-generator */
import type { EmptyViewContainer } from "./EmptyViewContainer";
import type { ViewComponent } from "@/lib/viewComponentContext";

declare global {
interface FlowViewComponents {
EmptyViewContainer: ViewComponent<typeof EmptyViewContainer>;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const PaginationInfos: FC<TextProps> = (props) => {

const list = useList();
const pagination = list.batches;
const isInitiallyLoading = list.loader.useIsInitiallyLoading();
const isLoading = list.loader.useIsLoading();
const isEmpty = list.useIsEmpty();

const totalItemsCount = pagination.getTotalItemsCount();
Expand All @@ -21,7 +21,7 @@ export const PaginationInfos: FC<TextProps> = (props) => {
return null;
}

const text = isInitiallyLoading ? (
const text = isLoading ? (
<SkeletonView width="200px" />
) : (
stringFormatter.format("list.paginationInfo", {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { FC } from "react";
import React from "react";
import styles from "./Header.module.css";
import clsx from "clsx";
import { ActiveFilters } from "@/components/List/components/Header/components/ActiveFilters";
Expand All @@ -18,6 +17,11 @@ export const Header: FC<PropsWithClassName> = (props) => {
const { className } = props;
const list = useList();

const isEmpty = list.useIsEmpty();
const isInitiallyLoading = list.loader.useIsInitiallyLoading();
const isDisabled =
isInitiallyLoading || (isEmpty && list.getEmptyViewType() === "list");

const availableViewModes = useAvailableViewModes();

const hasOptions =
Expand All @@ -38,16 +42,18 @@ export const Header: FC<PropsWithClassName> = (props) => {
<TunnelExit id="actions" />
{hasOptions && (
<DivView className={styles.options}>
<ViewModeContextMenu />
<SortingContextMenu />
<FilterContextMenus />
<AllFiltersModal />
<ViewModeContextMenu isDisabled={isDisabled} />
<SortingContextMenu isDisabled={isDisabled} />
<FilterContextMenus isDisabled={isDisabled} />
<AllFiltersModal isDisabled={isDisabled} />

{list.search && <SearchField search={list.search} />}
{list.search && (
<SearchField search={list.search} isDisabled={isDisabled} />
)}
</DivView>
)}
</DivView>
<ActiveFilters />
<ActiveFilters isDisabled={isDisabled} />
</DivView>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import locales from "../../../../locales/*.locale.json";
import { Translate } from "@/lib/react/components/Translate";
import { observer } from "mobx-react-lite";
import { useLocalizedStringFormatter } from "react-aria";
import { TooltipTrigger } from "@/components/Tooltip";
import {
IconClose,
IconSave,
Expand All @@ -21,7 +20,12 @@ import { Filter } from "@/components/List/model/filter/Filter";
import { transformDateValueToFormattedDate } from "@/lib/date/transformDateValueToFormattedDate";
import { DateRangeFilter } from "@/components/List/model/filter/DateRangeFilter";

export const ActiveFilters: FC = observer(() => {
interface Props {
isDisabled?: boolean;
}

export const ActiveFilters: FC<Props> = observer((props) => {
const { isDisabled } = props;
const list = useList();
const formatter = useLocalizedStringFormatter(locales);

Expand All @@ -30,7 +34,11 @@ export const ActiveFilters: FC = observer(() => {
const value = f.getValue();
if (value) {
return [
<BadgeView key={f.name} onClose={() => f.clear()}>
<BadgeView
key={f.name}
onClose={() => f.clear()}
isDisabled={isDisabled}
>
<TextView>
{`${transformDateValueToFormattedDate(value.start)} - ${transformDateValueToFormattedDate(value.end)}`}
</TextView>
Expand All @@ -42,7 +50,11 @@ export const ActiveFilters: FC = observer(() => {
return f.values
.filter((v) => v.isActive)
.map((v) => (
<BadgeView key={v.id} onClose={() => v.deactivate()}>
<BadgeView
key={v.id}
onClose={() => v.deactivate()}
isDisabled={isDisabled}
>
<TextView>{v.render()}</TextView>
</BadgeView>
));
Expand All @@ -52,7 +64,7 @@ export const ActiveFilters: FC = observer(() => {
const hasChanges = list.filters.some((f) => f.hasChanges());

const storeFiltersButton = storingAvailable && hasChanges && (
<TooltipTriggerView>
<TooltipTriggerView isDisabled={isDisabled}>
<TooltipView>
<Translate locales={locales}>list.filters.store</Translate>
</TooltipView>
Expand All @@ -71,7 +83,7 @@ export const ActiveFilters: FC = observer(() => {
);

const resetFiltersButton = hasChanges ? (
<TooltipTrigger>
<TooltipTriggerView isDisabled={isDisabled}>
<TooltipView>
<Translate locales={locales}>list.filters.reset</Translate>
</TooltipView>
Expand All @@ -84,12 +96,12 @@ export const ActiveFilters: FC = observer(() => {
>
<IconUndo />
</ButtonView>
</TooltipTrigger>
</TooltipTriggerView>
) : undefined;

const removeAllFiltersButton =
activeFilters.length > 1 ? (
<TooltipTrigger>
<TooltipTriggerView isDisabled={isDisabled}>
<TooltipView>
<Translate locales={locales}>list.filters.clear</Translate>
</TooltipView>
Expand All @@ -101,7 +113,7 @@ export const ActiveFilters: FC = observer(() => {
>
<IconClose />
</ButtonView>
</TooltipTrigger>
</TooltipTriggerView>
) : undefined;

if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ import Modal, { ModalTrigger } from "@/components/Modal";
import { SkeletonText } from "@/components/SkeletonText";
import { useAvailableViewModes } from "../../lib";

export const AllFiltersModal: FC = () => {
interface Props {
isDisabled?: boolean;
}

export const AllFiltersModal: FC<Props> = (props) => {
const { isDisabled } = props;
const list = useList();
const stringFormatter = useLocalizedStringFormatter(locales);

Expand Down Expand Up @@ -60,6 +65,7 @@ export const AllFiltersModal: FC = () => {
)}
variant="outline"
color="secondary"
isDisabled={isDisabled}
>
<TextView>{stringFormatter.format("list.filters.all")}</TextView>
<IconFilter />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import type { AnyDateRangeFilter } from "@/components/List/model/filter/types";

interface Props {
filter: AnyDateRangeFilter;
isDisabled?: boolean;
}

export const DateRangeFilterPopover: FC<Props> = (props) => {
const { filter } = props;
const { filter, isDisabled } = props;

const { name, property } = filter;

Expand All @@ -30,6 +31,7 @@ export const DateRangeFilterPopover: FC<Props> = (props) => {
className={headerStyles.hideOnMobile}
variant="outline"
color="secondary"
isDisabled={isDisabled}
>
<TextView>{name ?? property}</TextView>
<IconFilter />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import ContextMenuView from "@/views/ContextMenuView";

interface Props {
filter: Filter;
isDisabled?: boolean;
}

export const FilterContextMenu: FC<Props> = (props) => {
const { filter } = props;
const { filter, isDisabled } = props;

const { values, mode, name, property } = filter;

Expand All @@ -31,6 +32,7 @@ export const FilterContextMenu: FC<Props> = (props) => {
className={styles.hideOnMobile}
variant="outline"
color="secondary"
isDisabled={isDisabled}
>
<TextView>{name ?? property}</TextView>
<IconFilter />
Expand Down
Loading
Loading