@linagora/twake-mui 10.5.1 → 11.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/CHANGELOG.md +27 -0
- package/dist/components/ActionsMenu/Actions/call.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/call.js +16 -0
- package/dist/components/ActionsMenu/Actions/copyToClipboard.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/copyToClipboard.js +32 -0
- package/dist/components/ActionsMenu/Actions/divider.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/divider.js +7 -0
- package/dist/components/ActionsMenu/Actions/download.d.ts +15 -0
- package/dist/components/ActionsMenu/Actions/download.js +18 -0
- package/dist/components/ActionsMenu/Actions/editAttribute.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/editAttribute.js +12 -0
- package/dist/components/ActionsMenu/Actions/emailTo.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/emailTo.js +16 -0
- package/dist/components/ActionsMenu/Actions/exportToText.d.ts +6 -0
- package/dist/components/ActionsMenu/Actions/exportToText.js +28 -0
- package/dist/components/ActionsMenu/Actions/favorites.d.ts +12 -0
- package/dist/components/ActionsMenu/Actions/favorites.js +44 -0
- package/dist/components/ActionsMenu/Actions/helpers.d.ts +24 -0
- package/dist/components/ActionsMenu/Actions/helpers.js +128 -0
- package/dist/components/ActionsMenu/Actions/logger.d.ts +3 -0
- package/dist/components/ActionsMenu/Actions/logger.js +2 -0
- package/dist/components/ActionsMenu/Actions/makeAction.d.ts +11 -0
- package/dist/components/ActionsMenu/Actions/makeAction.js +19 -0
- package/dist/components/ActionsMenu/Actions/modify.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/modify.js +21 -0
- package/dist/components/ActionsMenu/Actions/others.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/others.js +11 -0
- package/dist/components/ActionsMenu/Actions/pdfHelpers.d.ts +35 -0
- package/dist/components/ActionsMenu/Actions/pdfHelpers.js +85 -0
- package/dist/components/ActionsMenu/Actions/print.d.ts +13 -0
- package/dist/components/ActionsMenu/Actions/print.js +42 -0
- package/dist/components/ActionsMenu/Actions/smsTo.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/smsTo.js +16 -0
- package/dist/components/ActionsMenu/Actions/viewInContacts.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/viewInContacts.js +21 -0
- package/dist/components/ActionsMenu/Actions/viewInDrive.d.ts +2 -0
- package/dist/components/ActionsMenu/Actions/viewInDrive.js +22 -0
- package/dist/components/ActionsMenu/ActionsItems.d.ts +17 -0
- package/dist/components/ActionsMenu/ActionsItems.js +27 -0
- package/dist/components/ActionsMenu/ActionsMenu.stories.d.ts +16 -0
- package/dist/components/ActionsMenu/ActionsMenu.stories.js +168 -0
- package/dist/components/ActionsMenu/ActionsMenuButton.d.ts +8 -0
- package/dist/components/ActionsMenu/ActionsMenuButton.js +18 -0
- package/dist/components/ActionsMenu/ActionsMenuItem.d.ts +16 -0
- package/dist/components/ActionsMenu/ActionsMenuItem.js +23 -0
- package/dist/components/ActionsMenu/ActionsMenuMobileHeader.d.ts +6 -0
- package/dist/components/ActionsMenu/ActionsMenuMobileHeader.js +15 -0
- package/dist/components/ActionsMenu/ActionsMenuWrapper.d.ts +11 -0
- package/dist/components/ActionsMenu/ActionsMenuWrapper.js +41 -0
- package/dist/components/ActionsMenu/helpers.d.ts +8 -0
- package/dist/components/ActionsMenu/helpers.js +21 -0
- package/dist/components/ActionsMenu/index.d.ts +21 -0
- package/dist/components/ActionsMenu/index.js +30 -0
- package/dist/components/ActionsMenu/locales/de.json +32 -0
- package/dist/components/ActionsMenu/locales/en.json +32 -0
- package/dist/components/ActionsMenu/locales/es.json +32 -0
- package/dist/components/ActionsMenu/locales/fr.json +32 -0
- package/dist/components/ActionsMenu/locales/index.d.ts +231 -0
- package/dist/components/ActionsMenu/locales/index.js +15 -0
- package/dist/components/ActionsMenu/locales/it.json +32 -0
- package/dist/components/ActionsMenu/locales/ru.json +32 -0
- package/dist/components/ActionsMenu/locales/vi.json +32 -0
- package/dist/components/ActionsMenu/types.d.ts +85 -0
- package/dist/components/ActionsMenu/types.js +1 -0
- package/dist/components/BottomSheet/index.js +5 -2
- package/dist/index.d.ts +38 -0
- package/dist/index.js +24 -0
- package/dist/lib/overrides.js +5 -0
- package/package.json +5 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Printer } from '@linagora/twake-icons';
|
|
2
|
+
import { makeBase64FromFile, makePdfBlob } from './helpers';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
import { makeAction } from './makeAction';
|
|
5
|
+
import { getActionsI18n } from '../locales';
|
|
6
|
+
export const print = ({ client, fetchBlobFileById, isFile }) => {
|
|
7
|
+
const { t } = getActionsI18n();
|
|
8
|
+
return makeAction({
|
|
9
|
+
name: 'print',
|
|
10
|
+
label: t('ActionsMenu.print'),
|
|
11
|
+
icon: Printer,
|
|
12
|
+
disabled: docs => docs.length === 0,
|
|
13
|
+
displayCondition: docs => docs.every(doc => isFile(doc)),
|
|
14
|
+
action: async (docs, { webviewIntent }) => {
|
|
15
|
+
const [firstDoc] = docs;
|
|
16
|
+
const isSingleDoc = docs.length === 1;
|
|
17
|
+
try {
|
|
18
|
+
if (webviewIntent) {
|
|
19
|
+
const blob = isSingleDoc
|
|
20
|
+
? await fetchBlobFileById(client, firstDoc._id)
|
|
21
|
+
: await makePdfBlob({ client, docs, fetchBlobFileById });
|
|
22
|
+
const base64 = await makeBase64FromFile(blob);
|
|
23
|
+
return await webviewIntent.call('print', base64);
|
|
24
|
+
}
|
|
25
|
+
const docUrl = isSingleDoc
|
|
26
|
+
? await client
|
|
27
|
+
.collection('io.cozy.files')
|
|
28
|
+
.getDownloadLinkById(firstDoc._id, firstDoc.name)
|
|
29
|
+
: URL.createObjectURL(await makePdfBlob({ client, docs, fetchBlobFileById }));
|
|
30
|
+
// Safari blocks window.open called from an async function
|
|
31
|
+
setTimeout(() => {
|
|
32
|
+
window.open(docUrl, '_blank');
|
|
33
|
+
});
|
|
34
|
+
return docUrl;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.error(`Error trying to print document ${webviewIntent ? 'inside' : 'outside'} the native app: ${JSON.stringify(error)}`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Comment } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const smsTo = () => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'smsTo',
|
|
8
|
+
label: t('ActionsMenu.smsTo'),
|
|
9
|
+
icon: Comment,
|
|
10
|
+
action: (docs) => {
|
|
11
|
+
const target = docs[0]?.phone?.[0]?.number;
|
|
12
|
+
if (target)
|
|
13
|
+
window.open(`sms:${target}`, '_self');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Openapp } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAppWebLink } from './helpers';
|
|
3
|
+
import { makeAction } from './makeAction';
|
|
4
|
+
import { getActionsI18n } from '../locales';
|
|
5
|
+
export const viewInContacts = ({ client, generateWebLink }) => {
|
|
6
|
+
const { t } = getActionsI18n();
|
|
7
|
+
return makeAction({
|
|
8
|
+
name: 'viewInContacts',
|
|
9
|
+
label: t('ActionsMenu.viewInContacts'),
|
|
10
|
+
icon: Openapp,
|
|
11
|
+
action: (docs) => {
|
|
12
|
+
const webLink = makeAppWebLink({
|
|
13
|
+
client,
|
|
14
|
+
generateWebLink,
|
|
15
|
+
slug: 'contacts',
|
|
16
|
+
hash: `/${docs[0]._id}`
|
|
17
|
+
});
|
|
18
|
+
window.open(webLink, '_blank');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { FolderOpen } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAppWebLink } from './helpers';
|
|
3
|
+
import { makeAction } from './makeAction';
|
|
4
|
+
import { getActionsI18n } from '../locales';
|
|
5
|
+
export const viewInDrive = ({ client, generateWebLink }) => {
|
|
6
|
+
const { t } = getActionsI18n();
|
|
7
|
+
return makeAction({
|
|
8
|
+
name: 'viewInDrive',
|
|
9
|
+
label: t('ActionsMenu.viewInDrive'),
|
|
10
|
+
icon: FolderOpen,
|
|
11
|
+
action: (docs) => {
|
|
12
|
+
const { dir_id: dirId, driveId } = docs[0];
|
|
13
|
+
const webLink = makeAppWebLink({
|
|
14
|
+
client,
|
|
15
|
+
generateWebLink,
|
|
16
|
+
slug: 'drive',
|
|
17
|
+
hash: driveId ? `shareddrive/${driveId}/${dirId}` : `folder/${dirId}`
|
|
18
|
+
});
|
|
19
|
+
window.open(webLink, '_blank');
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ListItemSize } from '../ListItem';
|
|
3
|
+
import { ActionComponent, ActionDocument, ActionObject } from './types';
|
|
4
|
+
export interface ActionsItemsProps {
|
|
5
|
+
docs: ActionDocument[];
|
|
6
|
+
actions: ActionObject[];
|
|
7
|
+
/** Options passed to the `action` method of every action */
|
|
8
|
+
actionOptions?: Record<string, unknown>;
|
|
9
|
+
/** Renders every action with this component instead of its own */
|
|
10
|
+
component?: ActionComponent;
|
|
11
|
+
/** Called after any action is clicked */
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
isListItem?: boolean;
|
|
14
|
+
size?: ListItemSize;
|
|
15
|
+
}
|
|
16
|
+
declare const ActionsItems: React.ForwardRefExoticComponent<ActionsItemsProps & React.RefAttributes<HTMLElement>>;
|
|
17
|
+
export default ActionsItems;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { forwardRef, useMemo } from 'react';
|
|
2
|
+
import { useWebviewIntent } from 'cozy-intent';
|
|
3
|
+
import { useI18n } from 'twake-i18n';
|
|
4
|
+
import { getActionName, getOnlyNeededActions } from './helpers';
|
|
5
|
+
const ActionsItems = forwardRef(({ docs, actions, actionOptions, component, onClick: overriddenClick, ...props }, ref) => {
|
|
6
|
+
const webviewIntent = useWebviewIntent();
|
|
7
|
+
const { t } = useI18n();
|
|
8
|
+
const cleanedActions = useMemo(() => getOnlyNeededActions(actions, docs), [actions, docs]);
|
|
9
|
+
return cleanedActions.map((actionObject, idx) => {
|
|
10
|
+
const actionName = getActionName(actionObject);
|
|
11
|
+
const actionDefinition = actionObject[actionName];
|
|
12
|
+
const { Component: ActionComponent, disabled } = actionDefinition;
|
|
13
|
+
const handleClick = (clickProps) => {
|
|
14
|
+
actionDefinition.action?.(docs, {
|
|
15
|
+
t,
|
|
16
|
+
webviewIntent,
|
|
17
|
+
...actionOptions,
|
|
18
|
+
...clickProps
|
|
19
|
+
});
|
|
20
|
+
overriddenClick?.();
|
|
21
|
+
};
|
|
22
|
+
const Component = component ?? ActionComponent;
|
|
23
|
+
return (React.createElement(Component, { ...props, ref: ref, key: actionName + idx, action: actionDefinition, docs: docs, autoFocus: idx === 0, disabled: disabled?.(docs), onClick: handleClick }));
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
ActionsItems.displayName = 'ActionsItems';
|
|
27
|
+
export default ActionsItems;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { PopoverOrigin } from '@mui/material';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
interface ExampleArgs {
|
|
4
|
+
defaultOpen: boolean;
|
|
5
|
+
vertical: PopoverOrigin['vertical'];
|
|
6
|
+
horizontal: PopoverOrigin['horizontal'];
|
|
7
|
+
autoClose: boolean;
|
|
8
|
+
autoCloseOnContextMenu: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare const meta: Meta<ExampleArgs>;
|
|
11
|
+
export default meta;
|
|
12
|
+
type Story = StoryObj<typeof meta>;
|
|
13
|
+
export declare const Default: Story;
|
|
14
|
+
export declare const ManualActions: Story;
|
|
15
|
+
export declare const Screenshot: Story;
|
|
16
|
+
export declare const ManualScreenshot: Story;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Attachment, Attention, Contrast, Copy, File, FileTypeText, Icon, Pen, People, Telephone } from '@linagora/twake-icons';
|
|
2
|
+
import { Box, Divider, ListItemIcon, Typography } from '@mui/material';
|
|
3
|
+
import { Globals } from '@react-spring/web';
|
|
4
|
+
import React, { forwardRef, useRef, useState } from 'react';
|
|
5
|
+
import DropdownButton from '../DropdownButton';
|
|
6
|
+
import ListItemText from '../ListItemText';
|
|
7
|
+
import { call } from './Actions/call';
|
|
8
|
+
import { divider } from './Actions/divider';
|
|
9
|
+
import { emailTo } from './Actions/emailTo';
|
|
10
|
+
import { makeActions } from './Actions/helpers';
|
|
11
|
+
import { modify } from './Actions/modify';
|
|
12
|
+
import { print } from './Actions/print';
|
|
13
|
+
import { smsTo } from './Actions/smsTo';
|
|
14
|
+
import { viewInContacts } from './Actions/viewInContacts';
|
|
15
|
+
import { viewInDrive } from './Actions/viewInDrive';
|
|
16
|
+
import ActionsMenuItem from './ActionsMenuItem';
|
|
17
|
+
import ActionsMenuMobileHeader from './ActionsMenuMobileHeader';
|
|
18
|
+
import ActionsMenuWrapper from './ActionsMenuWrapper';
|
|
19
|
+
import ActionsMenu from './index';
|
|
20
|
+
const doc = {
|
|
21
|
+
_id: 'id01',
|
|
22
|
+
_type: 'io.cozy.contacts',
|
|
23
|
+
phone: [{ number: '0102030405' }],
|
|
24
|
+
email: [{ address: 'johndoe@example.com' }]
|
|
25
|
+
};
|
|
26
|
+
const mockClient = {
|
|
27
|
+
getStackClient: () => ({ uri: 'https://' }),
|
|
28
|
+
getInstanceOptions: () => ({ subdomain: '' }),
|
|
29
|
+
collection: () => ({ getDownloadLinkById: () => Promise.resolve('') })
|
|
30
|
+
};
|
|
31
|
+
const LONG_TEXT = 'This is a custom action, with a very long text to show how it is displayed';
|
|
32
|
+
const CustomActionComponent = forwardRef((props, ref) => (React.createElement(ActionsMenuItem, { ...props, ref: ref },
|
|
33
|
+
React.createElement(ListItemIcon, null,
|
|
34
|
+
React.createElement(Icon, { icon: File })),
|
|
35
|
+
React.createElement(ListItemText, { primary: LONG_TEXT, secondary: LONG_TEXT }))));
|
|
36
|
+
CustomActionComponent.displayName = 'customAction';
|
|
37
|
+
const customAction = () => ({
|
|
38
|
+
name: 'customAction',
|
|
39
|
+
action: () => undefined,
|
|
40
|
+
Component: CustomActionComponent
|
|
41
|
+
});
|
|
42
|
+
const actions = makeActions([
|
|
43
|
+
modify,
|
|
44
|
+
viewInContacts,
|
|
45
|
+
viewInDrive,
|
|
46
|
+
divider,
|
|
47
|
+
call,
|
|
48
|
+
smsTo,
|
|
49
|
+
emailTo,
|
|
50
|
+
print,
|
|
51
|
+
divider,
|
|
52
|
+
customAction
|
|
53
|
+
], {
|
|
54
|
+
client: mockClient,
|
|
55
|
+
generateWebLink: () => '',
|
|
56
|
+
fetchBlobFileById: () => Promise.resolve(new Blob()),
|
|
57
|
+
isFile: () => false
|
|
58
|
+
});
|
|
59
|
+
const MobileHeader = () => (React.createElement(ActionsMenuMobileHeader, null,
|
|
60
|
+
React.createElement(ListItemIcon, null,
|
|
61
|
+
React.createElement(Icon, { icon: FileTypeText, size: 32 })),
|
|
62
|
+
React.createElement(ListItemText, { primary: "Title", slotProps: { primary: { variant: 'h6' } } })));
|
|
63
|
+
const ExampleActionsMenu = ({ defaultOpen, vertical, horizontal, autoClose, autoCloseOnContextMenu }) => {
|
|
64
|
+
const anchorRef = useRef(null);
|
|
65
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
66
|
+
return (React.createElement(React.Fragment, null,
|
|
67
|
+
React.createElement(DropdownButton, { ref: anchorRef, "aria-haspopup": "true", onClick: () => setIsOpen(open => !open) }, "Show action menu"),
|
|
68
|
+
React.createElement(ActionsMenu, { ref: anchorRef, open: isOpen, docs: [doc], actions: actions, anchorOrigin: { vertical, horizontal }, autoClose: autoClose, autoCloseOnContextMenu: autoCloseOnContextMenu, autoFocus: !defaultOpen, disablePortal: defaultOpen, disableScrollLock: defaultOpen, onClose: () => setIsOpen(false) },
|
|
69
|
+
React.createElement(MobileHeader, null))));
|
|
70
|
+
};
|
|
71
|
+
const ExampleManualActionsMenu = ({ defaultOpen }) => {
|
|
72
|
+
const [anchorEl, setAnchorEl] = useState(null);
|
|
73
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
74
|
+
return (React.createElement(React.Fragment, null,
|
|
75
|
+
React.createElement(DropdownButton, { ref: setAnchorEl, "aria-haspopup": "true", onClick: () => setIsOpen(open => !open) }, "Show action menu"),
|
|
76
|
+
isOpen && anchorEl !== null && (React.createElement(ActionsMenuWrapper, { open: true, anchorEl: anchorEl, anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' }, keepMounted: true, autoClose: true, autoFocus: !defaultOpen, disablePortal: defaultOpen, disableScrollLock: defaultOpen, onClose: () => setIsOpen(false) },
|
|
77
|
+
React.createElement(MobileHeader, null),
|
|
78
|
+
React.createElement(ActionsMenuItem, { autoFocus: true },
|
|
79
|
+
React.createElement(ListItemIcon, null,
|
|
80
|
+
React.createElement(Icon, { icon: Pen })),
|
|
81
|
+
React.createElement(ListItemText, { primary: "Modify" }),
|
|
82
|
+
React.createElement(ListItemIcon, null,
|
|
83
|
+
React.createElement(Typography, { color: "error" },
|
|
84
|
+
React.createElement(Icon, { icon: Attention })))),
|
|
85
|
+
React.createElement(ActionsMenuItem, null,
|
|
86
|
+
React.createElement(ListItemIcon, null,
|
|
87
|
+
React.createElement(Icon, { icon: People })),
|
|
88
|
+
React.createElement(ListItemText, { primary: "People" })),
|
|
89
|
+
React.createElement(ActionsMenuItem, null,
|
|
90
|
+
React.createElement(ListItemIcon, null,
|
|
91
|
+
React.createElement(Icon, { icon: Attachment })),
|
|
92
|
+
React.createElement(ListItemText, { primary: "Attachment" })),
|
|
93
|
+
React.createElement(ActionsMenuItem, null,
|
|
94
|
+
React.createElement(ListItemText, { primary: "Item without icon" })),
|
|
95
|
+
React.createElement(Divider, { sx: { my: 1 } }),
|
|
96
|
+
React.createElement(ActionsMenuItem, null,
|
|
97
|
+
React.createElement(ListItemIcon, null,
|
|
98
|
+
React.createElement(Icon, { icon: Telephone })),
|
|
99
|
+
React.createElement(ListItemText, { primary: "Call" })),
|
|
100
|
+
React.createElement(ActionsMenuItem, null,
|
|
101
|
+
React.createElement(ListItemIcon, null,
|
|
102
|
+
React.createElement(Icon, { icon: Contrast })),
|
|
103
|
+
React.createElement(ListItemText, { primary: "Contrast" })),
|
|
104
|
+
React.createElement(ActionsMenuItem, null,
|
|
105
|
+
React.createElement(ListItemIcon, null,
|
|
106
|
+
React.createElement(Icon, { icon: Copy })),
|
|
107
|
+
React.createElement(ListItemText, { primary: "Copy" }))))));
|
|
108
|
+
};
|
|
109
|
+
/** The bottom sheet animates through react-spring, which Playwright cannot fast-forward */
|
|
110
|
+
const skipSpringAnimations = () => {
|
|
111
|
+
Globals.assign({ skipAnimation: true });
|
|
112
|
+
return () => Globals.assign({ skipAnimation: false });
|
|
113
|
+
};
|
|
114
|
+
const meta = {
|
|
115
|
+
title: 'ActionsMenu',
|
|
116
|
+
tags: ['autodocs'],
|
|
117
|
+
parameters: {
|
|
118
|
+
docs: {
|
|
119
|
+
description: {
|
|
120
|
+
component: 'Menu of actions built with `makeActions`, displayed as a bottom sheet on mobile. `ActionsMenuMobileHeader` only renders on mobile, where the menu hides its anchor.'
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
argTypes: {
|
|
125
|
+
defaultOpen: { control: 'boolean' },
|
|
126
|
+
vertical: { control: 'select', options: ['top', 'center', 'bottom'] },
|
|
127
|
+
horizontal: { control: 'select', options: ['left', 'center', 'right'] },
|
|
128
|
+
autoClose: { control: 'boolean' },
|
|
129
|
+
autoCloseOnContextMenu: { control: 'boolean' }
|
|
130
|
+
},
|
|
131
|
+
args: {
|
|
132
|
+
defaultOpen: false,
|
|
133
|
+
vertical: 'bottom',
|
|
134
|
+
horizontal: 'left',
|
|
135
|
+
autoClose: true,
|
|
136
|
+
autoCloseOnContextMenu: true
|
|
137
|
+
},
|
|
138
|
+
render: args => React.createElement(ExampleActionsMenu, { ...args })
|
|
139
|
+
};
|
|
140
|
+
export default meta;
|
|
141
|
+
export const Default = {};
|
|
142
|
+
export const ManualActions = {
|
|
143
|
+
parameters: {
|
|
144
|
+
docs: {
|
|
145
|
+
description: {
|
|
146
|
+
story: 'Items written by hand inside `ActionsMenuWrapper`, without `makeActions`.'
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
render: args => React.createElement(ExampleManualActionsMenu, { defaultOpen: args.defaultOpen })
|
|
151
|
+
};
|
|
152
|
+
// Visual Regression - the menu built with makeActions, opened. Kept out of the
|
|
153
|
+
// docs page, where its open bottom sheet would cover the whole page on mobile
|
|
154
|
+
export const Screenshot = {
|
|
155
|
+
tags: ['argos', '!autodocs'],
|
|
156
|
+
parameters: { layout: 'padded' },
|
|
157
|
+
beforeEach: skipSpringAnimations,
|
|
158
|
+
render: args => (React.createElement(Box, { sx: { minHeight: 640 } },
|
|
159
|
+
React.createElement(ExampleActionsMenu, { ...args, defaultOpen: true })))
|
|
160
|
+
};
|
|
161
|
+
// Visual Regression - the menu written by hand, opened
|
|
162
|
+
export const ManualScreenshot = {
|
|
163
|
+
tags: ['argos', '!autodocs'],
|
|
164
|
+
parameters: { layout: 'padded' },
|
|
165
|
+
beforeEach: skipSpringAnimations,
|
|
166
|
+
render: () => (React.createElement(Box, { sx: { minHeight: 440 } },
|
|
167
|
+
React.createElement(ExampleManualActionsMenu, { defaultOpen: true })))
|
|
168
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActionDocument, ActionObject } from './types';
|
|
3
|
+
export interface ActionsMenuButtonProps {
|
|
4
|
+
docs?: ActionDocument[];
|
|
5
|
+
actions?: ActionObject[];
|
|
6
|
+
}
|
|
7
|
+
declare const ActionsMenuButton: ({ docs, actions }: ActionsMenuButtonProps) => React.JSX.Element;
|
|
8
|
+
export default ActionsMenuButton;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Dots, Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { IconButton, ListItemIcon } from '@mui/material';
|
|
3
|
+
import React, { useRef, useState } from 'react';
|
|
4
|
+
import { useExtendI18n, useI18n } from 'twake-i18n';
|
|
5
|
+
import ActionsMenu from '.';
|
|
6
|
+
import { locales } from './locales';
|
|
7
|
+
const ActionsMenuButton = ({ docs, actions }) => {
|
|
8
|
+
const [showActions, setShowActions] = useState(false);
|
|
9
|
+
const actionsRef = useRef(null);
|
|
10
|
+
useExtendI18n(locales);
|
|
11
|
+
const { t } = useI18n();
|
|
12
|
+
return (React.createElement(React.Fragment, null,
|
|
13
|
+
React.createElement(ListItemIcon, null,
|
|
14
|
+
React.createElement(IconButton, { ref: actionsRef, "aria-label": t('ActionsMenu.menu'), onClick: () => setShowActions(true) },
|
|
15
|
+
React.createElement(Icon, { icon: Dots }))),
|
|
16
|
+
showActions && (React.createElement(ActionsMenu, { open: true, ref: actionsRef, docs: docs, actions: actions, anchorOrigin: { vertical: 'bottom', horizontal: 'right' }, onClose: () => setShowActions(false) }))));
|
|
17
|
+
};
|
|
18
|
+
export default ActionsMenuButton;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MenuItemProps } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ListItemSize } from '../ListItem';
|
|
4
|
+
import { Action, ActionDocument } from './types';
|
|
5
|
+
export interface ActionsMenuItemProps extends Omit<MenuItemProps, 'ref' | 'onClick' | 'action'> {
|
|
6
|
+
/** Action rendered by the item, kept off the DOM */
|
|
7
|
+
action?: Action;
|
|
8
|
+
/** Documents the action applies to, kept off the DOM */
|
|
9
|
+
docs?: ActionDocument[];
|
|
10
|
+
/** Renders a list item, as in a bottom sheet, instead of a menu item */
|
|
11
|
+
isListItem?: boolean;
|
|
12
|
+
size?: ListItemSize;
|
|
13
|
+
onClick?: (clickProps?: object) => void;
|
|
14
|
+
}
|
|
15
|
+
declare const ActionsMenuItem: React.ForwardRefExoticComponent<ActionsMenuItemProps & React.RefAttributes<HTMLElement>>;
|
|
16
|
+
export default ActionsMenuItem;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MenuItem } from '@mui/material';
|
|
2
|
+
import React, { Children, cloneElement, forwardRef, isValidElement } from 'react';
|
|
3
|
+
import ListItemButton from '../ListItemButton';
|
|
4
|
+
import ListItemText from '../ListItemText';
|
|
5
|
+
const NON_DOM_PROPS = ['t', 'f', 'lang'];
|
|
6
|
+
const omitNonDomProps = (props) => {
|
|
7
|
+
return Object.fromEntries(Object.entries(props).filter(([key]) => !NON_DOM_PROPS.includes(key)));
|
|
8
|
+
};
|
|
9
|
+
const mergeSx = (style, sx) => {
|
|
10
|
+
return [style, sx ?? false].flat();
|
|
11
|
+
};
|
|
12
|
+
const ActionsMenuItem = forwardRef(({ action: _action, docs: _docs, isListItem, size, sx, children, ...props }, ref) => {
|
|
13
|
+
const domProps = omitNonDomProps(props);
|
|
14
|
+
if (isListItem) {
|
|
15
|
+
return (React.createElement(ListItemButton, { ...domProps, ref: ref, size: size, ellipsis: false, sx: mergeSx({ wordBreak: 'break-word' }, sx) }, children));
|
|
16
|
+
}
|
|
17
|
+
return (React.createElement(MenuItem, { ...domProps, ref: ref, sx: mergeSx({ minWidth: 256 }, sx) }, Children.map(children, child => isValidElement(child) &&
|
|
18
|
+
child.type === ListItemText
|
|
19
|
+
? cloneElement(child, { ellipsis: false })
|
|
20
|
+
: child)));
|
|
21
|
+
});
|
|
22
|
+
ActionsMenuItem.displayName = 'ActionsMenuItem';
|
|
23
|
+
export default ActionsMenuItem;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ActionsMenuItemProps } from './ActionsMenuItem';
|
|
3
|
+
export type ActionsMenuMobileHeaderProps = Omit<ActionsMenuItemProps, 'isListItem'>;
|
|
4
|
+
/** Gives context to the actions, only on mobile where the menu hides its anchor */
|
|
5
|
+
declare const ActionsMenuMobileHeader: React.ForwardRefExoticComponent<ActionsMenuMobileHeaderProps & React.RefAttributes<HTMLElement>>;
|
|
6
|
+
export default ActionsMenuMobileHeader;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Divider } from '@mui/material';
|
|
2
|
+
import React, { forwardRef } from 'react';
|
|
3
|
+
import ActionsMenuItem from './ActionsMenuItem';
|
|
4
|
+
import { useBreakpoints } from '../../hooks/useBreakpoints';
|
|
5
|
+
/** Gives context to the actions, only on mobile where the menu hides its anchor */
|
|
6
|
+
const ActionsMenuMobileHeader = forwardRef(({ children, ...props }, ref) => {
|
|
7
|
+
const { isMobile } = useBreakpoints();
|
|
8
|
+
if (!isMobile)
|
|
9
|
+
return null;
|
|
10
|
+
return (React.createElement(React.Fragment, null,
|
|
11
|
+
React.createElement(ActionsMenuItem, { ...props, ref: ref, isListItem: true }, children),
|
|
12
|
+
React.createElement(Divider, { sx: { my: 1 } })));
|
|
13
|
+
});
|
|
14
|
+
ActionsMenuMobileHeader.displayName = 'ActionsMenuMobileHeader';
|
|
15
|
+
export default ActionsMenuMobileHeader;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MenuProps } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
export interface ActionsMenuWrapperProps extends Omit<MenuProps, 'open' | 'onClose'> {
|
|
4
|
+
open?: boolean;
|
|
5
|
+
/** Closes the menu when an item is clicked */
|
|
6
|
+
autoClose?: boolean;
|
|
7
|
+
onClose?: () => void;
|
|
8
|
+
}
|
|
9
|
+
/** Renders its items in a menu on desktop, in a bottom sheet on mobile */
|
|
10
|
+
declare const ActionsMenuWrapper: ({ children, autoClose, open, onClose, ...props }: ActionsMenuWrapperProps) => React.JSX.Element | null;
|
|
11
|
+
export default ActionsMenuWrapper;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Menu, Paper } from '@mui/material';
|
|
2
|
+
import React, { Children, cloneElement, isValidElement } from 'react';
|
|
3
|
+
import { useBreakpoints } from '../../hooks/useBreakpoints';
|
|
4
|
+
import BottomSheet from '../BottomSheet';
|
|
5
|
+
const getComponentName = (node) => {
|
|
6
|
+
if (!isValidElement(node) || typeof node.type === 'string')
|
|
7
|
+
return null;
|
|
8
|
+
const type = node.type;
|
|
9
|
+
return type.displayName ?? type.name ?? null;
|
|
10
|
+
};
|
|
11
|
+
/** Renders its items in a menu on desktop, in a bottom sheet on mobile */
|
|
12
|
+
const ActionsMenuWrapper = ({ children, autoClose, open = false, onClose, ...props }) => {
|
|
13
|
+
const { isMobile } = useBreakpoints();
|
|
14
|
+
const makeClickHandler = (itemProps) => (clickProps) => {
|
|
15
|
+
itemProps.onClick?.(clickProps);
|
|
16
|
+
if (autoClose)
|
|
17
|
+
onClose?.();
|
|
18
|
+
};
|
|
19
|
+
if (isMobile) {
|
|
20
|
+
if (!open)
|
|
21
|
+
return null;
|
|
22
|
+
return (React.createElement(BottomSheet, { backdrop: true, onClose: onClose },
|
|
23
|
+
React.createElement(Paper, { square: true }, Children.map(children, child => isValidElement(child)
|
|
24
|
+
? cloneElement(child, {
|
|
25
|
+
isListItem: true,
|
|
26
|
+
size: 'small',
|
|
27
|
+
onClick: makeClickHandler(child.props)
|
|
28
|
+
})
|
|
29
|
+
: null))));
|
|
30
|
+
}
|
|
31
|
+
const items = Children.toArray(children);
|
|
32
|
+
// Keeps the focus on the first action, as the mobile header renders nothing here
|
|
33
|
+
const isFirstItemMobileHeader = getComponentName(items[0]) === 'ActionsMenuMobileHeader';
|
|
34
|
+
return (React.createElement(Menu, { ...props, open: open, onClose: onClose }, Children.map(children, (child, idx) => isValidElement(child)
|
|
35
|
+
? cloneElement(child, {
|
|
36
|
+
autoFocus: isFirstItemMobileHeader && idx === 1 ? true : undefined,
|
|
37
|
+
onClick: makeClickHandler(child.props)
|
|
38
|
+
})
|
|
39
|
+
: null)));
|
|
40
|
+
};
|
|
41
|
+
export default ActionsMenuWrapper;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ActionDocument, ActionObject } from './types';
|
|
2
|
+
export declare const getActionName: (actionObject: ActionObject) => string;
|
|
3
|
+
/**
|
|
4
|
+
* Keeps the actions to display: drops hidden ones, the same action twice in a
|
|
5
|
+
* row, and a trailing divider. Display conditions are only known with the docs,
|
|
6
|
+
* so the final list cannot be known when the actions are made.
|
|
7
|
+
*/
|
|
8
|
+
export declare const getOnlyNeededActions: (actions: ActionObject[], docs: ActionDocument[]) => ActionObject[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const getActionName = (actionObject) => {
|
|
2
|
+
return Object.keys(actionObject)[0];
|
|
3
|
+
};
|
|
4
|
+
const isDivider = (actionObject) => {
|
|
5
|
+
const name = getActionName(actionObject);
|
|
6
|
+
return name === 'hr' || name === 'divider';
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Keeps the actions to display: drops hidden ones, the same action twice in a
|
|
10
|
+
* row, and a trailing divider. Display conditions are only known with the docs,
|
|
11
|
+
* so the final list cannot be known when the actions are made.
|
|
12
|
+
*/
|
|
13
|
+
export const getOnlyNeededActions = (actions, docs) => {
|
|
14
|
+
const displayableActions = actions.filter(actionObject => {
|
|
15
|
+
const { displayCondition } = Object.values(actionObject)[0];
|
|
16
|
+
return !displayCondition || displayCondition(docs);
|
|
17
|
+
});
|
|
18
|
+
const withoutRepeats = displayableActions.filter((actionObject, idx) => idx === 0 ||
|
|
19
|
+
getActionName(actionObject) !== getActionName(displayableActions[idx - 1]));
|
|
20
|
+
return withoutRepeats.filter((actionObject, idx) => !(isDivider(actionObject) && idx === withoutRepeats.length - 1));
|
|
21
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { PopoverOrigin } from '@mui/material';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ActionsItemsProps } from './ActionsItems';
|
|
4
|
+
import { ActionsMenuWrapperProps } from './ActionsMenuWrapper';
|
|
5
|
+
import { ActionDocument, ActionObject } from './types';
|
|
6
|
+
export interface ActionsMenuComponentsProps {
|
|
7
|
+
actionsItems?: Pick<ActionsItemsProps, 'actionOptions' | 'onClick'>;
|
|
8
|
+
}
|
|
9
|
+
export interface ActionsMenuProps extends ActionsMenuWrapperProps {
|
|
10
|
+
docs?: ActionDocument[];
|
|
11
|
+
actions?: ActionObject[];
|
|
12
|
+
anchorOrigin?: PopoverOrigin;
|
|
13
|
+
autoCloseOnContextMenu?: boolean;
|
|
14
|
+
componentsProps?: ActionsMenuComponentsProps;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Menu of actions, displayed as a bottom sheet on mobile. The `ref` is the
|
|
18
|
+
* element the menu is anchored to on desktop.
|
|
19
|
+
*/
|
|
20
|
+
declare const ActionsMenu: React.ForwardRefExoticComponent<Omit<ActionsMenuProps, "ref"> & React.RefAttributes<HTMLElement>>;
|
|
21
|
+
export default ActionsMenu;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import ActionsItems from './ActionsItems';
|
|
3
|
+
import ActionsMenuWrapper from './ActionsMenuWrapper';
|
|
4
|
+
const DEFAULT_ANCHOR_ORIGIN = {
|
|
5
|
+
vertical: 'bottom',
|
|
6
|
+
horizontal: 'left'
|
|
7
|
+
};
|
|
8
|
+
const computeTransformOrigin = ({ vertical, horizontal }) => {
|
|
9
|
+
const opposites = {
|
|
10
|
+
bottom: 'top',
|
|
11
|
+
top: 'bottom'
|
|
12
|
+
};
|
|
13
|
+
return { vertical: opposites[vertical] ?? vertical, horizontal };
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Menu of actions, displayed as a bottom sheet on mobile. The `ref` is the
|
|
17
|
+
* element the menu is anchored to on desktop.
|
|
18
|
+
*/
|
|
19
|
+
const ActionsMenu = forwardRef(({ docs = [], actions = [], anchorOrigin = DEFAULT_ANCHOR_ORIGIN, autoClose = true, autoCloseOnContextMenu = true, componentsProps = {}, children, onClose, ...props }, ref) => {
|
|
20
|
+
const getAnchorEl = () => ref && typeof ref === 'object' ? ref.current : null;
|
|
21
|
+
const handleContextMenu = (event) => {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
onClose?.();
|
|
24
|
+
};
|
|
25
|
+
return (React.createElement(ActionsMenuWrapper, { ...props, anchorEl: getAnchorEl, anchorOrigin: anchorOrigin, transformOrigin: computeTransformOrigin(anchorOrigin), keepMounted: true, autoClose: autoClose, ...(autoCloseOnContextMenu && { onContextMenu: handleContextMenu }), onClose: onClose },
|
|
26
|
+
children,
|
|
27
|
+
React.createElement(ActionsItems, { ...componentsProps.actionsItems, docs: docs, actions: actions })));
|
|
28
|
+
});
|
|
29
|
+
ActionsMenu.displayName = 'ActionsMenu';
|
|
30
|
+
export default ActionsMenu;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ActionsMenu": {
|
|
3
|
+
"menu": "Menü",
|
|
4
|
+
"viewInContacts": "In Cozy Contacts anzeigen",
|
|
5
|
+
"viewInDrive": "Öffnen",
|
|
6
|
+
"modify": "Bearbeiten",
|
|
7
|
+
"emailTo": "E-Mail senden",
|
|
8
|
+
"smsTo": "Nachricht senden",
|
|
9
|
+
"print": "Drucken",
|
|
10
|
+
"download": "Herunterladen",
|
|
11
|
+
"favorites": {
|
|
12
|
+
"add": {
|
|
13
|
+
"label": "Zu Favoriten hinzufügen",
|
|
14
|
+
"success": "%{filename} wurde zu den Favoriten hinzugefügt |||| Diese Elemente wurden zu den Favoriten hinzugefügt"
|
|
15
|
+
},
|
|
16
|
+
"remove": {
|
|
17
|
+
"label": "Aus Favoriten entfernen",
|
|
18
|
+
"success": "%{filename} wurde aus den Favoriten entfernt |||| Diese Elemente wurden aus den Favoriten entfernt"
|
|
19
|
+
},
|
|
20
|
+
"error": "Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."
|
|
21
|
+
},
|
|
22
|
+
"others": "Weitere",
|
|
23
|
+
"editAttribute": "Attribut bearbeiten",
|
|
24
|
+
"copyToClipboard": {
|
|
25
|
+
"copy": "Kopieren",
|
|
26
|
+
"success": "In die Zwischenablage kopiert",
|
|
27
|
+
"error": "Kopieren in die Zwischenablage nicht möglich"
|
|
28
|
+
},
|
|
29
|
+
"call": "Anrufen",
|
|
30
|
+
"exportToText": "Als Text exportieren"
|
|
31
|
+
}
|
|
32
|
+
}
|