@strato-admin/cloudscape 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/Admin.d.ts +17 -0
- package/dist/Admin.js +69 -0
- package/dist/RecordLink.d.ts +9 -0
- package/dist/RecordLink.js +43 -0
- package/dist/__mocks__/strato-core.js +50 -0
- package/dist/__mocks__to__delete/strato-core.js +50 -0
- package/dist/button/BulkDeleteButton.d.ts +7 -0
- package/dist/button/BulkDeleteButton.js +17 -0
- package/dist/button/Button.d.ts +6 -0
- package/dist/button/Button.js +6 -0
- package/dist/button/CreateButton.d.ts +6 -0
- package/dist/button/CreateButton.js +24 -0
- package/dist/button/EditButton.d.ts +8 -0
- package/dist/button/EditButton.js +24 -0
- package/dist/button/SaveButton.d.ts +6 -0
- package/dist/button/SaveButton.js +8 -0
- package/dist/button/index.d.ts +5 -0
- package/dist/button/index.js +5 -0
- package/dist/collection-hooks/index.d.ts +2 -0
- package/dist/collection-hooks/index.js +2 -0
- package/dist/collection-hooks/interfaces.d.ts +93 -0
- package/dist/collection-hooks/interfaces.js +1 -0
- package/dist/collection-hooks/useCollection.d.ts +3 -0
- package/dist/collection-hooks/useCollection.js +102 -0
- package/dist/create/Create.d.ts +40 -0
- package/dist/create/Create.js +34 -0
- package/dist/create/CreateHeader.d.ts +7 -0
- package/dist/create/CreateHeader.js +18 -0
- package/dist/create/index.d.ts +2 -0
- package/dist/create/index.js +2 -0
- package/dist/detail/KeyValuePairs.d.ts +36 -0
- package/dist/detail/KeyValuePairs.js +58 -0
- package/dist/detail/Show.d.ts +39 -0
- package/dist/detail/Show.js +40 -0
- package/dist/detail/ShowHeader.d.ts +7 -0
- package/dist/detail/ShowHeader.js +19 -0
- package/dist/detail/index.d.ts +3 -0
- package/dist/detail/index.js +3 -0
- package/dist/edit/Edit.d.ts +42 -0
- package/dist/edit/Edit.js +38 -0
- package/dist/edit/EditHeader.d.ts +7 -0
- package/dist/edit/EditHeader.js +18 -0
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +2 -0
- package/dist/field/ArrayField.d.ts +29 -0
- package/dist/field/ArrayField.js +30 -0
- package/dist/field/BadgeField.d.ts +12 -0
- package/dist/field/BadgeField.js +15 -0
- package/dist/field/BooleanField.d.ts +18 -0
- package/dist/field/BooleanField.js +14 -0
- package/dist/field/CurrencyField.d.ts +19 -0
- package/dist/field/CurrencyField.js +23 -0
- package/dist/field/DateField.d.ts +14 -0
- package/dist/field/DateField.js +17 -0
- package/dist/field/IdField.d.ts +17 -0
- package/dist/field/IdField.js +21 -0
- package/dist/field/NumberField.d.ts +14 -0
- package/dist/field/NumberField.js +18 -0
- package/dist/field/ReferenceField.d.ts +16 -0
- package/dist/field/ReferenceField.js +23 -0
- package/dist/field/ReferenceManyField.d.ts +55 -0
- package/dist/field/ReferenceManyField.js +19 -0
- package/dist/field/StatusIndicatorField.d.ts +56 -0
- package/dist/field/StatusIndicatorField.js +48 -0
- package/dist/field/TextField.d.ts +5 -0
- package/dist/field/TextField.js +11 -0
- package/dist/field/index.d.ts +23 -0
- package/dist/field/index.js +23 -0
- package/dist/field/types.d.ts +56 -0
- package/dist/field/types.js +1 -0
- package/dist/form/Form.d.ts +13 -0
- package/dist/form/Form.js +33 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +2 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/input/AttributeEditor.d.ts +25 -0
- package/dist/input/AttributeEditor.js +80 -0
- package/dist/input/AutocompleteInput.d.ts +10 -0
- package/dist/input/AutocompleteInput.js +67 -0
- package/dist/input/FieldTitle.d.ts +8 -0
- package/dist/input/FieldTitle.js +29 -0
- package/dist/input/FormField.d.ts +7 -0
- package/dist/input/FormField.js +35 -0
- package/dist/input/FormFieldContext.d.ts +6 -0
- package/dist/input/FormFieldContext.js +3 -0
- package/dist/input/NumberInput.d.ts +7 -0
- package/dist/input/NumberInput.js +27 -0
- package/dist/input/ReferenceInput.d.ts +3 -0
- package/dist/input/ReferenceInput.js +25 -0
- package/dist/input/SelectInput.d.ts +15 -0
- package/dist/input/SelectInput.js +47 -0
- package/dist/input/SliderInput.d.ts +6 -0
- package/dist/input/SliderInput.js +25 -0
- package/dist/input/TextAreaInput.d.ts +6 -0
- package/dist/input/TextAreaInput.js +23 -0
- package/dist/input/TextInput.d.ts +7 -0
- package/dist/input/TextInput.js +23 -0
- package/dist/input/index.d.ts +11 -0
- package/dist/input/index.js +11 -0
- package/dist/input/types.d.ts +6 -0
- package/dist/input/types.js +1 -0
- package/dist/layout/AppLayout.d.ts +8 -0
- package/dist/layout/AppLayout.js +38 -0
- package/dist/layout/TopNavigation.d.ts +6 -0
- package/dist/layout/TopNavigation.js +53 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +2 -0
- package/dist/list/Cards.d.ts +11 -0
- package/dist/list/Cards.js +27 -0
- package/dist/list/List.d.ts +43 -0
- package/dist/list/List.js +28 -0
- package/dist/list/Table.d.ts +112 -0
- package/dist/list/Table.examples.d.ts +1 -0
- package/dist/list/Table.examples.js +3 -0
- package/dist/list/Table.js +218 -0
- package/dist/list/TableHeader.d.ts +7 -0
- package/dist/list/TableHeader.js +22 -0
- package/dist/list/index.d.ts +4 -0
- package/dist/list/index.js +4 -0
- package/dist/preferences/index.d.ts +0 -0
- package/dist/preferences/index.js +1 -0
- package/dist/theme/ThemeManager.d.ts +2 -0
- package/dist/theme/ThemeManager.js +11 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/package.json +73 -0
- package/src/Admin.test.tsx +32 -0
- package/src/Admin.tsx +123 -0
- package/src/RecordLink.stories.tsx +56 -0
- package/src/RecordLink.tsx +67 -0
- package/src/__mocks__/strato-core.tsx +52 -0
- package/src/button/BulkDeleteButton.stories.tsx +59 -0
- package/src/button/BulkDeleteButton.test.tsx +64 -0
- package/src/button/BulkDeleteButton.tsx +41 -0
- package/src/button/Button.stories.tsx +31 -0
- package/src/button/Button.tsx +12 -0
- package/src/button/CreateButton.stories.tsx +42 -0
- package/src/button/CreateButton.tsx +38 -0
- package/src/button/EditButton.stories.tsx +29 -0
- package/src/button/EditButton.tsx +38 -0
- package/src/button/SaveButton.stories.tsx +35 -0
- package/src/button/SaveButton.tsx +19 -0
- package/src/button/index.ts +5 -0
- package/src/collection-hooks/index.ts +2 -0
- package/src/collection-hooks/interfaces.ts +80 -0
- package/src/collection-hooks/useCollection.test.ts +413 -0
- package/src/collection-hooks/useCollection.ts +125 -0
- package/src/create/Create.test.tsx +63 -0
- package/src/create/Create.tsx +93 -0
- package/src/create/CreateHeader.tsx +34 -0
- package/src/create/index.ts +2 -0
- package/src/detail/KeyValuePairs.test.tsx +98 -0
- package/src/detail/KeyValuePairs.tsx +107 -0
- package/src/detail/Show.test.tsx +96 -0
- package/src/detail/Show.tsx +104 -0
- package/src/detail/ShowHeader.test.tsx +80 -0
- package/src/detail/ShowHeader.tsx +35 -0
- package/src/detail/index.ts +3 -0
- package/src/edit/Edit.test.tsx +91 -0
- package/src/edit/Edit.tsx +102 -0
- package/src/edit/EditHeader.tsx +34 -0
- package/src/edit/index.ts +2 -0
- package/src/field/ArrayField.tsx +51 -0
- package/src/field/BadgeField.tsx +33 -0
- package/src/field/BooleanField.stories.tsx +56 -0
- package/src/field/BooleanField.test.tsx +63 -0
- package/src/field/BooleanField.tsx +42 -0
- package/src/field/CurrencyField.stories.tsx +67 -0
- package/src/field/CurrencyField.tsx +45 -0
- package/src/field/DateField.stories.tsx +67 -0
- package/src/field/DateField.tsx +33 -0
- package/src/field/IdField.test.tsx +88 -0
- package/src/field/IdField.tsx +40 -0
- package/src/field/NumberField.stories.tsx +75 -0
- package/src/field/NumberField.tsx +35 -0
- package/src/field/ReferenceField.test.tsx +88 -0
- package/src/field/ReferenceField.tsx +64 -0
- package/src/field/ReferenceManyField.test.tsx +41 -0
- package/src/field/ReferenceManyField.tsx +73 -0
- package/src/field/StatusIndicatorField.stories.tsx +93 -0
- package/src/field/StatusIndicatorField.test.tsx +143 -0
- package/src/field/StatusIndicatorField.tsx +119 -0
- package/src/field/TextField.stories.tsx +45 -0
- package/src/field/TextField.tsx +17 -0
- package/src/field/index.ts +23 -0
- package/src/field/types.ts +58 -0
- package/src/form/Form.test.tsx +55 -0
- package/src/form/Form.tsx +66 -0
- package/src/form/index.ts +2 -0
- package/src/index.ts +25 -0
- package/src/input/AttributeEditor.test.tsx +147 -0
- package/src/input/AttributeEditor.tsx +185 -0
- package/src/input/AutocompleteInput.test.tsx +178 -0
- package/src/input/AutocompleteInput.tsx +116 -0
- package/src/input/FieldTitle.tsx +53 -0
- package/src/input/FormField.tsx +87 -0
- package/src/input/FormFieldContext.ts +9 -0
- package/src/input/NumberInput.tsx +56 -0
- package/src/input/ReferenceInput.test.tsx +35 -0
- package/src/input/ReferenceInput.tsx +36 -0
- package/src/input/SelectInput.tsx +91 -0
- package/src/input/SliderInput.test.tsx +103 -0
- package/src/input/SliderInput.tsx +49 -0
- package/src/input/TextAreaInput.tsx +48 -0
- package/src/input/TextInput.test.tsx +91 -0
- package/src/input/TextInput.tsx +51 -0
- package/src/input/index.ts +11 -0
- package/src/input/types.ts +14 -0
- package/src/layout/AppLayout.test.tsx +87 -0
- package/src/layout/AppLayout.tsx +60 -0
- package/src/layout/TopNavigation.test.tsx +78 -0
- package/src/layout/TopNavigation.tsx +84 -0
- package/src/layout/index.ts +2 -0
- package/src/list/Cards.tsx +58 -0
- package/src/list/List.tsx +76 -0
- package/src/list/Table.examples.tsx +11 -0
- package/src/list/Table.stories.tsx +73 -0
- package/src/list/Table.test.tsx +255 -0
- package/src/list/Table.tsx +438 -0
- package/src/list/TableHeader.test.tsx +114 -0
- package/src/list/TableHeader.tsx +44 -0
- package/src/list/index.ts +4 -0
- package/src/preferences/index.ts +0 -0
- package/src/stories/Button.stories.ts +54 -0
- package/src/stories/Button.tsx +31 -0
- package/src/stories/Configure.mdx +369 -0
- package/src/stories/Header.stories.ts +34 -0
- package/src/stories/Header.tsx +47 -0
- package/src/stories/Page.stories.ts +33 -0
- package/src/stories/Page.tsx +71 -0
- package/src/stories/RaStoryDecorator.tsx +38 -0
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +1 -0
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +1 -0
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +1 -0
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +1 -0
- package/src/stories/assets/youtube.svg +1 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/theme/ThemeManager.tsx +15 -0
- package/src/theme/index.ts +2 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { AppLayout } from './AppLayout';
|
|
5
|
+
import { useDefaultTitle, useResourceDefinitions } from '@strato-admin/core';
|
|
6
|
+
import { TopNavigation } from './TopNavigation';
|
|
7
|
+
|
|
8
|
+
// Mock strato-core
|
|
9
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
10
|
+
|
|
11
|
+
// Mock global-styles
|
|
12
|
+
vi.mock('@cloudscape-design/global-styles', () => ({
|
|
13
|
+
Mode: { Light: 'light', Dark: 'dark' },
|
|
14
|
+
applyMode: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock react-router-dom
|
|
18
|
+
vi.mock('react-router-dom', () => ({
|
|
19
|
+
useNavigate: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock TopNavigation
|
|
23
|
+
vi.mock('./TopNavigation', () => ({
|
|
24
|
+
TopNavigation: vi.fn(() => <div data-testid="top-navigation" />),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Mock Cloudscape components
|
|
28
|
+
vi.mock('@cloudscape-design/components/app-layout', () => ({
|
|
29
|
+
default: ({ navigation, content }: any) => (
|
|
30
|
+
<div>
|
|
31
|
+
<div data-testid="navigation">{navigation}</div>
|
|
32
|
+
<div data-testid="content">{content}</div>
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock('@cloudscape-design/components/side-navigation', () => ({
|
|
38
|
+
default: () => <div data-testid="side-navigation" />,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
describe('AppLayout', () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should use title from useDefaultTitle hook if no title prop provided', () => {
|
|
47
|
+
(useDefaultTitle as any).mockReturnValue('Hook Title');
|
|
48
|
+
|
|
49
|
+
render(
|
|
50
|
+
<AppLayout>
|
|
51
|
+
<div>Content</div>
|
|
52
|
+
</AppLayout>,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(vi.mocked(TopNavigation).mock.calls[0][0]).toMatchObject({
|
|
56
|
+
identity: { title: 'Hook Title' },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should prioritize title prop over useDefaultTitle hook', () => {
|
|
61
|
+
(useDefaultTitle as any).mockReturnValue('Hook Title');
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<AppLayout title="Prop Title">
|
|
65
|
+
<div>Content</div>
|
|
66
|
+
</AppLayout>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(vi.mocked(TopNavigation).mock.calls[0][0]).toMatchObject({
|
|
70
|
+
identity: { title: 'Prop Title' },
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle undefined default title', () => {
|
|
75
|
+
(useDefaultTitle as any).mockReturnValue(undefined);
|
|
76
|
+
|
|
77
|
+
render(
|
|
78
|
+
<AppLayout>
|
|
79
|
+
<div>Content</div>
|
|
80
|
+
</AppLayout>,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(vi.mocked(TopNavigation).mock.calls[0][0]).toMatchObject({
|
|
84
|
+
identity: { title: '' },
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import CloudscapeAppLayout from '@cloudscape-design/components/app-layout';
|
|
3
|
+
import SideNavigation from '@cloudscape-design/components/side-navigation';
|
|
4
|
+
import { useResourceDefinitions, useDefaultTitle, useGetResourceLabel } from '@strato-admin/core';
|
|
5
|
+
import { useNavigate } from 'react-router-dom';
|
|
6
|
+
import { TopNavigation } from './TopNavigation';
|
|
7
|
+
import ThemeManager from '../theme/ThemeManager';
|
|
8
|
+
|
|
9
|
+
export interface AppLayoutProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
header?: React.ReactNode;
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AppLayout = ({ children, header, title }: AppLayoutProps) => {
|
|
16
|
+
const resources = useResourceDefinitions();
|
|
17
|
+
const getResourceLabel = useGetResourceLabel();
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const defaultTitle = useDefaultTitle();
|
|
20
|
+
const [navigationOpen, setNavigationOpen] = useState(true);
|
|
21
|
+
|
|
22
|
+
const appTitle =
|
|
23
|
+
title ?? (typeof defaultTitle === 'string' ? defaultTitle : '');
|
|
24
|
+
|
|
25
|
+
const items = Object.values(resources).map((resource) => ({
|
|
26
|
+
type: 'link' as const,
|
|
27
|
+
text: getResourceLabel(resource.name),
|
|
28
|
+
href: `/${resource.name}`,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<ThemeManager />
|
|
34
|
+
{header || <TopNavigation identity={{ title: appTitle, href: '/' }} />}
|
|
35
|
+
<CloudscapeAppLayout
|
|
36
|
+
headerSelector="#header"
|
|
37
|
+
navigationOpen={navigationOpen}
|
|
38
|
+
onNavigationChange={({ detail }) => setNavigationOpen(detail.open)}
|
|
39
|
+
navigation={
|
|
40
|
+
<SideNavigation
|
|
41
|
+
//header={{
|
|
42
|
+
// href: '/',
|
|
43
|
+
// text: 'Dashboard',
|
|
44
|
+
//}}
|
|
45
|
+
items={items}
|
|
46
|
+
onFollow={(event) => {
|
|
47
|
+
if (!event.detail.external) {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
navigate(event.detail.href);
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
}
|
|
54
|
+
content={<div>{children}</div>}
|
|
55
|
+
/>
|
|
56
|
+
</>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default AppLayout;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useAuthProvider } from '@strato-admin/core';
|
|
5
|
+
import CloudscapeTopNavigation from '@cloudscape-design/components/top-navigation';
|
|
6
|
+
import TopNavigation from './TopNavigation';
|
|
7
|
+
|
|
8
|
+
// Mock ra-core
|
|
9
|
+
vi.mock('@strato-admin/core', () => ({
|
|
10
|
+
useLocale: vi.fn(() => 'en'),
|
|
11
|
+
useSetLocale: vi.fn(),
|
|
12
|
+
useLocales: vi.fn(() => []),
|
|
13
|
+
useTranslate: vi.fn(() => (key: string, options: any) => options?._ || key),
|
|
14
|
+
useAuthProvider: vi.fn(),
|
|
15
|
+
useStore: vi.fn(() => ['light', vi.fn()]),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
// Mock Cloudscape components
|
|
19
|
+
vi.mock('@cloudscape-design/components/top-navigation', () => ({
|
|
20
|
+
default: vi.fn(() => <div data-testid="top-navigation" />),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock global-styles
|
|
24
|
+
vi.mock('@cloudscape-design/global-styles', () => ({
|
|
25
|
+
Mode: { Light: 'light', Dark: 'dark' },
|
|
26
|
+
applyMode: vi.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe('TopNavigation', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should display theme toggle', () => {
|
|
35
|
+
render(<TopNavigation />);
|
|
36
|
+
|
|
37
|
+
const navigationProps = (CloudscapeTopNavigation as any).mock.calls[0][0];
|
|
38
|
+
const themeToggle = navigationProps.utilities.find((u: any) => u.iconSvg !== undefined);
|
|
39
|
+
expect(themeToggle).toBeDefined();
|
|
40
|
+
expect(themeToggle.type).toBe('button');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should not display user menu when authProvider is missing', () => {
|
|
44
|
+
(useAuthProvider as any).mockReturnValue(undefined);
|
|
45
|
+
|
|
46
|
+
render(<TopNavigation />);
|
|
47
|
+
|
|
48
|
+
const navigationProps = (CloudscapeTopNavigation as any).mock.calls[0][0];
|
|
49
|
+
const userMenu = navigationProps.utilities.find((u: any) => u.iconName === 'user-profile');
|
|
50
|
+
expect(userMenu).toBeUndefined();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should display user menu when authProvider is present', () => {
|
|
54
|
+
(useAuthProvider as any).mockReturnValue({});
|
|
55
|
+
|
|
56
|
+
render(<TopNavigation />);
|
|
57
|
+
|
|
58
|
+
const navigationProps = (CloudscapeTopNavigation as any).mock.calls[0][0];
|
|
59
|
+
const userMenu = navigationProps.utilities.find((u: any) => u.iconName === 'user-profile');
|
|
60
|
+
expect(userMenu).toBeDefined();
|
|
61
|
+
expect(userMenu.text).toBe('User'); // Based on mock translate returning default value
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should use provided identity', () => {
|
|
65
|
+
render(<TopNavigation identity={{ title: 'Custom Title', href: '/custom' }} />);
|
|
66
|
+
|
|
67
|
+
const navigationProps = (CloudscapeTopNavigation as any).mock.calls[0][0];
|
|
68
|
+
expect(navigationProps.identity).toEqual({ title: 'Custom Title', href: '/custom' });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should use provided utilities', () => {
|
|
72
|
+
const customUtilities = [{ type: 'button' as const, text: 'Custom' }];
|
|
73
|
+
render(<TopNavigation utilities={customUtilities} />);
|
|
74
|
+
|
|
75
|
+
const navigationProps = (CloudscapeTopNavigation as any).mock.calls[0][0];
|
|
76
|
+
expect(navigationProps.utilities).toEqual(customUtilities);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import CloudscapeTopNavigation, { TopNavigationProps } from '@cloudscape-design/components/top-navigation';
|
|
2
|
+
import { useLocale, useSetLocale, useLocales, useTranslate, useAuthProvider, useStore } from '@strato-admin/core';
|
|
3
|
+
|
|
4
|
+
export interface MyTopNavigationProps extends Omit<TopNavigationProps, 'identity'> {
|
|
5
|
+
identity?: TopNavigationProps.Identity;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const LightModeIcon = (
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" focusable="false">
|
|
10
|
+
<path d="M8 1.5v13a6.5 6.5 0 0 0 0-13z" fill="currentColor" />
|
|
11
|
+
<circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
12
|
+
</svg>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const DarkModeIcon = (
|
|
16
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" focusable="false">
|
|
17
|
+
<path d="M8 1.5v13a6.5 6.5 0 0 1 0-13z" fill="currentColor" />
|
|
18
|
+
<circle cx="8" cy="8" r="7" fill="none" stroke="currentColor" stroke-width="1.5" />
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
export const TopNavigation = ({ utilities: providedUtilities, identity, ...props }: MyTopNavigationProps) => {
|
|
23
|
+
const locale = useLocale();
|
|
24
|
+
const setLocale = useSetLocale();
|
|
25
|
+
const locales = useLocales();
|
|
26
|
+
const translate = useTranslate();
|
|
27
|
+
const authProvider = useAuthProvider();
|
|
28
|
+
const [theme, setTheme] = useStore('theme', 'light');
|
|
29
|
+
|
|
30
|
+
let utilities = providedUtilities;
|
|
31
|
+
|
|
32
|
+
if (!utilities) {
|
|
33
|
+
const autoUtilities: any[] = [];
|
|
34
|
+
|
|
35
|
+
autoUtilities.push({
|
|
36
|
+
type: 'button',
|
|
37
|
+
iconSvg: theme === 'light' ? LightModeIcon : DarkModeIcon,
|
|
38
|
+
onClick: () => {
|
|
39
|
+
setTheme(theme === 'dark' ? 'light' : 'dark');
|
|
40
|
+
},
|
|
41
|
+
ariaLabel: translate('strato.action.toggle_theme', { _: 'Toggle theme' }),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (locales && locales.length > 1) {
|
|
45
|
+
autoUtilities.push({
|
|
46
|
+
type: 'menu-dropdown',
|
|
47
|
+
text: locales.find((l: any) => l.locale === locale)?.name || locale,
|
|
48
|
+
iconName: 'globe',
|
|
49
|
+
onItemClick: (event: any) => {
|
|
50
|
+
setLocale(event.detail.id);
|
|
51
|
+
},
|
|
52
|
+
items: locales.map((l: any) => ({
|
|
53
|
+
id: l.locale,
|
|
54
|
+
text: l.name,
|
|
55
|
+
})),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (authProvider) {
|
|
60
|
+
autoUtilities.push({
|
|
61
|
+
type: 'menu-dropdown',
|
|
62
|
+
text: translate('ra.auth.user_menu', { _: 'User' }),
|
|
63
|
+
iconName: 'user-profile',
|
|
64
|
+
items: [
|
|
65
|
+
{ id: 'profile', text: translate('ra.auth.profile', { _: 'Profile' }) },
|
|
66
|
+
{ id: 'signout', text: translate('ra.auth.logout', { _: 'Sign out' }) },
|
|
67
|
+
],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
utilities = autoUtilities;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div id="header">
|
|
75
|
+
<CloudscapeTopNavigation
|
|
76
|
+
identity={identity || { title: 'Strato Admin', href: '/' }}
|
|
77
|
+
utilities={utilities}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default TopNavigation;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import CloudscapeCards, { CardsProps } from '@cloudscape-design/components/cards';
|
|
3
|
+
import Pagination from '@cloudscape-design/components/pagination';
|
|
4
|
+
import { RaRecord, RecordContextProvider, useFieldSchema } from '@strato-admin/core';
|
|
5
|
+
import { useCollection } from '../collection-hooks';
|
|
6
|
+
import KeyValuePairs from '../detail/KeyValuePairs';
|
|
7
|
+
|
|
8
|
+
export interface ListCardsProps<T extends RaRecord = any> extends Omit<CardsProps<T>, 'items' | 'cardDefinition'> {
|
|
9
|
+
renderItem?: (item: T) => React.ReactNode;
|
|
10
|
+
include?: string[];
|
|
11
|
+
exclude?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const ListCards = <T extends RaRecord = any>({
|
|
15
|
+
renderItem,
|
|
16
|
+
include,
|
|
17
|
+
exclude,
|
|
18
|
+
...props
|
|
19
|
+
}: ListCardsProps<T>) => {
|
|
20
|
+
const { items, paginationProps, collectionProps } = useCollection<T>({
|
|
21
|
+
filtering: {},
|
|
22
|
+
pagination: {},
|
|
23
|
+
sorting: {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const schemaChildren = useFieldSchema();
|
|
27
|
+
|
|
28
|
+
const defaultRenderItem = (_item: T) => (
|
|
29
|
+
<KeyValuePairs include={include} exclude={exclude}>
|
|
30
|
+
{schemaChildren}
|
|
31
|
+
</KeyValuePairs>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const finalRenderItem = renderItem || defaultRenderItem;
|
|
35
|
+
|
|
36
|
+
const cardDefinition: CardsProps.CardDefinition<T> = {
|
|
37
|
+
sections: [
|
|
38
|
+
{
|
|
39
|
+
id: 'main',
|
|
40
|
+
content: (item: T) => <RecordContextProvider value={item}>{finalRenderItem(item) as any}</RecordContextProvider>,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<CloudscapeCards
|
|
47
|
+
{...collectionProps}
|
|
48
|
+
{...props}
|
|
49
|
+
items={items || []}
|
|
50
|
+
cardDefinition={cardDefinition}
|
|
51
|
+
pagination={<Pagination {...paginationProps} />}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Cards = ListCards;
|
|
57
|
+
|
|
58
|
+
export default ListCards;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ListBase, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
|
|
3
|
+
import Table from './Table';
|
|
4
|
+
|
|
5
|
+
export interface ListProps<_RecordType extends RaRecord = any> {
|
|
6
|
+
children?: React.ReactNode;
|
|
7
|
+
fieldSchema?: React.ReactNode;
|
|
8
|
+
include?: string[];
|
|
9
|
+
exclude?: string[];
|
|
10
|
+
title?: React.ReactNode;
|
|
11
|
+
actions?: React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to enable text filtering in the implicit Table.
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
filtering?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Whether to show the preferences button in the implicit Table.
|
|
19
|
+
* @default true
|
|
20
|
+
*/
|
|
21
|
+
preferences?: boolean | React.ReactNode;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A List component that provides a list context and a Cloudscape Table.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <List>
|
|
30
|
+
* <Table>
|
|
31
|
+
* <Table.Column source="name" />
|
|
32
|
+
* </Table>
|
|
33
|
+
* </List>
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Using FieldSchema from context
|
|
37
|
+
* <List include={['name', 'price']} />
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // Passing a custom field schema
|
|
41
|
+
* <List fieldSchema={<FieldSchema>...</FieldSchema>}>
|
|
42
|
+
* <Table />
|
|
43
|
+
* </List>
|
|
44
|
+
*/
|
|
45
|
+
export const List = <RecordType extends RaRecord = any>({
|
|
46
|
+
children,
|
|
47
|
+
fieldSchema,
|
|
48
|
+
include,
|
|
49
|
+
exclude,
|
|
50
|
+
title,
|
|
51
|
+
actions,
|
|
52
|
+
filtering = true,
|
|
53
|
+
preferences = true,
|
|
54
|
+
...props
|
|
55
|
+
}: ListProps<RecordType>) => {
|
|
56
|
+
const finalChildren = children || (
|
|
57
|
+
<Table
|
|
58
|
+
include={include}
|
|
59
|
+
exclude={exclude}
|
|
60
|
+
title={title}
|
|
61
|
+
actions={actions}
|
|
62
|
+
filtering={filtering}
|
|
63
|
+
preferences={preferences}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ListBase {...props}>
|
|
69
|
+
<ResourceSchemaProvider resource={props.resource} fieldSchema={fieldSchema}>
|
|
70
|
+
{finalChildren as any}
|
|
71
|
+
</ResourceSchemaProvider>
|
|
72
|
+
</ListBase>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default List;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Table, Column, NumberColumn, DateColumn } from './Table';
|
|
2
|
+
|
|
3
|
+
export const ProductList = () => (
|
|
4
|
+
<Table title="Product Catalog">
|
|
5
|
+
<Column source="name" label="Product Name" />
|
|
6
|
+
<Column source="category" label="Category" />
|
|
7
|
+
<NumberColumn source="price" label="Price" />
|
|
8
|
+
<NumberColumn source="stock" label="Inventory" />
|
|
9
|
+
<DateColumn source="lastUpdated" label="Last Updated" />
|
|
10
|
+
</Table>
|
|
11
|
+
);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { Table, Column, NumberColumn, DateColumn } from './Table';
|
|
4
|
+
import { ResourceContext, CoreAdminContext, ListContextProvider } from '@strato-admin/core';
|
|
5
|
+
import { ProductList } from './Table.examples';
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Table> = {
|
|
8
|
+
title: 'Components/Table',
|
|
9
|
+
component: Table,
|
|
10
|
+
tags: ['autodocs'],
|
|
11
|
+
decorators: [
|
|
12
|
+
(Story: React.ComponentType) => (
|
|
13
|
+
<CoreAdminContext dataProvider={{} as any}>
|
|
14
|
+
<ResourceContext.Provider value="products">
|
|
15
|
+
<Story />
|
|
16
|
+
</ResourceContext.Provider>
|
|
17
|
+
</CoreAdminContext>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof Table>;
|
|
24
|
+
|
|
25
|
+
const items = [
|
|
26
|
+
{ id: 1, name: 'Wireless Mouse', category: 'Accessories', price: 25.99, stock: 150, lastUpdated: '2024-03-10' },
|
|
27
|
+
{ id: 2, name: 'Mechanical Keyboard', category: 'Accessories', price: 89.99, stock: 45, lastUpdated: '2024-03-08' },
|
|
28
|
+
{ id: 3, name: '27-inch Monitor', category: 'Hardware', price: 299.99, stock: 20, lastUpdated: '2024-03-05' },
|
|
29
|
+
{ id: 4, name: 'USB-C Cable', category: 'Accessories', price: 12.5, stock: 500, lastUpdated: '2024-03-11' },
|
|
30
|
+
{ id: 5, name: 'Laptop Pro 14', category: 'Hardware', price: 1499.0, stock: 10, lastUpdated: '2024-03-01' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const listContext = {
|
|
34
|
+
data: items,
|
|
35
|
+
total: items.length,
|
|
36
|
+
isPending: false,
|
|
37
|
+
isFetching: false,
|
|
38
|
+
isLoading: false,
|
|
39
|
+
page: 1,
|
|
40
|
+
perPage: 10,
|
|
41
|
+
setPage: () => {},
|
|
42
|
+
setPerPage: () => {},
|
|
43
|
+
sort: { field: 'name', order: 'ASC' },
|
|
44
|
+
setSort: () => {},
|
|
45
|
+
filterValues: {},
|
|
46
|
+
setFilters: () => {},
|
|
47
|
+
selectedIds: [],
|
|
48
|
+
onSelect: () => {},
|
|
49
|
+
onToggleItem: () => {},
|
|
50
|
+
onUnselectItems: () => {},
|
|
51
|
+
resource: 'products',
|
|
52
|
+
} as any;
|
|
53
|
+
|
|
54
|
+
export const Basic: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<ListContextProvider value={listContext}>
|
|
57
|
+
<ProductList />
|
|
58
|
+
</ListContextProvider>
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const WithFiltering: Story = {
|
|
63
|
+
render: () => (
|
|
64
|
+
<ListContextProvider value={listContext}>
|
|
65
|
+
<Table title="Inventory Management" filtering preferences>
|
|
66
|
+
<Column source="name" label="Product Name" />
|
|
67
|
+
<Column source="category" label="Category" />
|
|
68
|
+
<NumberColumn source="price" label="Price" />
|
|
69
|
+
<NumberColumn source="stock" label="Inventory" />
|
|
70
|
+
</Table>
|
|
71
|
+
</ListContextProvider>
|
|
72
|
+
),
|
|
73
|
+
};
|