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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions apps/default-app/app-shell.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ export default {
},
header: {
actions: [
{
bundle: "@self/services/headerActions/CreateNewContentDropDownMenu.js",
},
{
bundle: "@hv/user-notifications-client/index.js",
config: {
Expand Down Expand Up @@ -248,8 +251,15 @@ export default {
},
],
},
providers: [
{ bundle: "@self/providers/DefaultAppProvider.js" },
{ bundle: "@hv/sample-app/providers/CandyAppProvider.js" },
],
services: {
"default-app/services:UseCreateNewContentAction": [
{
bundle: "default-app/services/create/useCreateNewReportAction.js",
ranking: 100,
},
{
bundle: "bundle-to-fail.js",
},
],
},
} satisfies HvAppShellConfig;
2 changes: 2 additions & 0 deletions apps/default-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
"@emotion/styled": "^11.10.5",
"@hitachivantara/app-shell-events": "*",
"@hitachivantara/app-shell-navigation": "*",
"@hitachivantara/app-shell-services": "*",
"@hitachivantara/app-shell-shared": "*",
"@hitachivantara/uikit-react-core": "*",
"@hitachivantara/uikit-react-icons": "*",
"@mui/material": "^5.12.3",
"@phosphor-icons/react": "^2.1.10",
"@types/react-table": "^7.7.14",
"clsx": "^2.0.0",
"i18next": "^24.2.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useTranslation } from "react-i18next";

import { UseCreateNewContentAction } from "../types/UseCreateNewContentAction";

const useCreateNewReportAction: UseCreateNewContentAction = () => {
const { t } = useTranslation();

return {
id: "default-app/actions/createNewReport",
label: t("action.createNewReport.label"),
onAction: () => {
console.log("Creating a new report...");
},
};
};

export default useCreateNewReportAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { FC, useCallback, useMemo } from "react";
import { PlusCircleIcon } from "@phosphor-icons/react";
import { useServices } from "@hitachivantara/app-shell-services";
import {
HvDropDownMenu,
HvDropDownMenuProps,
HvIconContainer,
HvListValue,
} from "@hitachivantara/uikit-react-core";

import { ServiceDefinitions } from "../serviceDefinition";
import {
CreateNewContentAction,
UseCreateNewContentAction,
} from "../types/UseCreateNewContentAction";

type OnDropDownMenuClickCallback = NonNullable<HvDropDownMenuProps["onClick"]>;

function isNonNull<T>(value: T): value is NonNullable<T> {
return value != null;
}

function createListValue(action: CreateNewContentAction): HvListValue {
return {
id: action.id,
label: action.label,
icon: action.icon,
} as HvListValue;
}

const findAction = (
actions: (CreateNewContentAction | undefined)[],
dataListItem: HvListValue,
): CreateNewContentAction | undefined => {
return actions
.filter(isNonNull)
.find((action) => action.id === dataListItem.id);
};

const CreateNewContentDropDownMenuInner: FC<{
actionHooks: UseCreateNewContentAction[];
}> = ({ actionHooks }) => {
const actionResults = actionHooks.map((actionHook) => actionHook());

// Filter out undefined actions and create menu items
const dataList = useMemo(() => {
return actionResults.filter(isNonNull).map(createListValue);
}, [actionResults]);

const onClick = useCallback<OnDropDownMenuClickCallback>(
(_event, dataListItem) => {
const action = findAction(actionResults, dataListItem);
if (action) {
action.onAction();
}
},
[actionResults],
);

return dataList.length > 0 ? (
<HvDropDownMenu
icon={
<HvIconContainer size="sm">
<PlusCircleIcon />
</HvIconContainer>
}
dataList={dataList}
keepOpened={false}
onClick={onClick}
/>
) : null;
};

// The host component for UseCreateNewContentAction services.
const CreateNewContentDropDownMenu: FC = () => {
// Get the service hooks for the Create New Content actions.
const {
services: actionHooks,
isPending,
error,
} = useServices<UseCreateNewContentAction>(
ServiceDefinitions.UseCreateNewContentAction.id,
);

// While pending or on error, do not render anything.
if (isPending || error) {
if (error) {
console.warn("Unable to load UseCreateNewContent actions: ", error);
}
return null;
}

return <CreateNewContentDropDownMenuInner actionHooks={actionHooks} />;
};

export default CreateNewContentDropDownMenu;
26 changes: 26 additions & 0 deletions apps/default-app/src/services/serviceDefinition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export const ServiceDefinitions = {
/**
* The {@const UseCreateNewContentAction} service represents an action for
* creating new content from the default-app application.
*
* Create new content actions are displayed on a menu or dropdown button.
*
* Instances of this service are React hook functions of type
* {@link UseCreateNewContentAction}.
*/
UseCreateNewContentAction: {
id: "default-app/services:UseCreateNewContentAction",
},

/**
* The {@const UseContentTypeInfo} service represents UI information for a
* type of the given content of a given application, including the content
* type's identifier, label, description and icon.
*
* Instances of this service are React hook functions, of type
* {@link UseContentTypeInfo}.
*/
UseContentTypeInfo: {
id: "default-app/services:UseContentTypeInfo",
},
};
25 changes: 25 additions & 0 deletions apps/default-app/src/services/types/UseContentTypeInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ReactNode } from "react";

export type ContentTypeInfo = {
/**
* The id of the content type
*/
id: string;

/**
* The label of the content type.
*/
label: string;

/**
* The description of the content type.
*/
description?: string;

/**
* The icon of the content type, as a ReactNode.
*/
icon: ReactNode;
};

export type UseContentTypeInfo = () => ContentTypeInfo;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ReactNode } from "react";

export type CreateNewContentAction = {
id: string;
ordinal?: number;
label: string;
icon?: ReactNode;
onAction: () => void;
};

export type UseCreateNewContentAction = () =>
| CreateNewContentAction
| undefined;
2 changes: 2 additions & 0 deletions apps/default-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default defineConfig(({ mode }) => ({
"src/pages/Theming",
"src/pages/TabLayout",
"src/pages/DisplayDefaultAppContext",
"src/services/headerActions/CreateNewContentDropDownMenu",
"src/services/create/useCreateNewReportAction",
],
}),
],
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/src/content/app-shell/base-concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ These are the current shared dependencies and their versions:
| `react-router-dom` | `^6.9.0` |
| `@emotion/cache`, `@emotion/react` | `^11.11.0` |
| `@hitachivantara/uikit-react-shared` | `latest` |
| `@hitachivantara/app-shell-shared` | `latest` |
| `@hitachivantara/app-shell-services` | `latest` |

## Shared Modules

Expand All @@ -49,6 +51,7 @@ The **App Shell** itself is able to load the following types of _Shared Modules_
- **View**: Module exporting a React Component intended to be rendered in a **App Shell** panel (usually the main panel, as a page). It's configured in the **App Shell** configuration file in the `mainPanel.views` array. Detailed in the [next section](./routing).
- **Header Action**: Module exporting a React Component that provides a button (or other small UI component) to be rendered in the right-hand side of the **App Shell** header. It's configured in the **App Shell** configuration file in the `header.actions` array. Detailed in the [Header Actions](./header-actions) section.
- **Provider**: Module exporting a React Component that provides a React Context to be used by other _Shared Modules_. It's configured in the **App Shell** configuration file in the [`providers`](./configuration#providers) array.
- - **Service**: Modules that follows a provider/consumer model and is handled by the `@hitachivantara/app-shell-services` package. It's configured in the **App Shell** configuration file in the [`services`](./configuration#services) object.
- **Theme**: Module that exports a UI Kit theme definition. They can be referenced in the **App Shell** configuration file in the [`theming`](./configuration#theming) section.

All the above _Shared Modules_ types assume its subject is exported as the default export of the module.
42 changes: 42 additions & 0 deletions apps/docs/src/content/app-shell/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,48 @@ Notes:
- The **App Shell** will instantiate the _Providers_ in the order they are declared in the configuration file. However, dependencies between _Providers_ should be avoided, and never rely on the existence of another _Provider_.
- This shouldn't be abused: Context only needed for the rendering of a _View_ shouldn't be put on a global _Provider_ just because another _View_ needs it. E.g. the i18next provider should continue to be instantiated on every view, using a HOC for example, instead of having a global provider for each _Application Bundle_, each with its own i18next provider always instanced in case it is needed.

### `services`

This prop defines the _Services_ that are to be handled by the `@hitachivantara/app-shell-services` package.

The services property is a key-value object, where the key is the service ID and the value is an array of service definitions:

- _InstanceServiceConfig_: Inline, already-instantiated service value (function, hook, object) provided directly in the config. Useful for local or static implementations.
- _BundleServiceConfig_: Points to a JS bundle/module to be loaded dynamically at runtime. The bundle exports the service (hook, etc.) and follows the service contract defined by the consumer.
- _FactoryServiceConfig_: A factory-style provider: a function (or a bundle that exports one) that is invoked to create the service instance. Supports on-demand instantiation and configuration injection.
- _ComponentServiceConfig_: Specifically for React component providers (a component or a bundle that exports a component). Useful when consumers expect renderable components.

```ts
const appShellConfig = {
services: {
"my-app/services:UseCreateAction": [
// Instance
{ instance: myLocalHookOrFn, ranking: 10 },

// Bundle
{
bundle: "some-app/actions/create/useCreateReportAction.js",
ranking: 50,
},

// Factory
{
factory: {
bundle: "some-app/factories/factory.js",
config: { foo: 1 },
},
},

// Component
{
component: { bundle: "some-app/ui/MyButton.js" },
ranking: 50,
},
],
},
};
```

### `translations`

This property defines bundles of translations that need to be added in runtime to **App Shell** translation bundle.
Expand Down
36 changes: 36 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"lint:fix": "oxlint --fix",
"typecheck": "tsc --noEmit",
"prepare": "husky install .config/husky",
"publish-dry": "lerna publish --no-push --registry \"http://localhost:4873/\"",
"publish-dry": "lerna publish --no-push --no-changelog --no-git-tag-version --registry \"http://localhost:4873/\"",
"publish": "lerna publish"
},
"devDependencies": {
Expand Down
Loading
Loading