@linagora/twake-mui 10.5.0 → 11.0.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/components/SpaceOverlay/SpaceOverlay.d.ts +1 -1
- package/dist/index.d.ts +40 -2
- package/dist/index.js +25 -1
- package/package.json +5 -1
- /package/dist/components/SpaceOverlay/{spaceOverlay.d.ts → connectSpaceOverlay.d.ts} +0 -0
- /package/dist/components/SpaceOverlay/{spaceOverlay.js → connectSpaceOverlay.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
# @linagora/twake-mui [11.0.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.5.1...@linagora/twake-mui@11.0.0) (2026-10-08)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **twake-mui:** Reposition BottomSheet when the window is resized ([7036375](https://github.com/linagora/twake-ui/commit/70363755a1f61e2ca77d1fa3ac9132e3e33fd00e))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **twake-mui:** Add ActionsMenu locales ([f0e09cb](https://github.com/linagora/twake-ui/commit/f0e09cb0f6ae7e8c3e988bc5b62fc7190c07f475))
|
|
12
|
+
* **twake-mui:** Migrate ActionsMenu from cozy-ui ([7b12778](https://github.com/linagora/twake-ui/commit/7b12778d86948e5c53fe9b2e16555ed966e64dc6))
|
|
13
|
+
* **twake-mui:** Migrate ActionsMenu predefined actions from cozy-ui ([7f66436](https://github.com/linagora/twake-ui/commit/7f6643633dfa1d6e65f402ee26af223e3ac5d440))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### BREAKING CHANGES
|
|
17
|
+
|
|
18
|
+
* **twake-mui:** cozy-intent is now a required peer dependency of
|
|
19
|
+
@linagora/twake-mui. Apps must install cozy-intent >=2.29.1.
|
|
20
|
+
|
|
21
|
+
## @linagora/twake-mui [10.5.1](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.5.0...@linagora/twake-mui@10.5.1) (2026-10-07)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* **twake-mui:** Rename spaceOverlay.ts to connectSpaceOverlay.ts ([20754d1](https://github.com/linagora/twake-ui/commit/20754d1efdc12bf1f40331042de0ff16035f6aee))
|
|
27
|
+
|
|
1
28
|
# @linagora/twake-mui [10.5.0](https://github.com/linagora/twake-ui/compare/@linagora/twake-mui@10.4.1...@linagora/twake-mui@10.5.0) (2026-10-07)
|
|
2
29
|
|
|
3
30
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Telephone } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const call = () => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'call',
|
|
8
|
+
label: t('ActionsMenu.call'),
|
|
9
|
+
icon: Telephone,
|
|
10
|
+
action: (docs) => {
|
|
11
|
+
const target = docs[0]?.phone?.[0]?.number;
|
|
12
|
+
if (target)
|
|
13
|
+
window.open(`tel:${target}`, '_self');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Copy } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const copyToClipboard = () => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'copyToClipboard',
|
|
8
|
+
label: t('ActionsMenu.copyToClipboard.copy'),
|
|
9
|
+
icon: Copy,
|
|
10
|
+
action: async (_docs, { showAlert, copyValue }) => {
|
|
11
|
+
if (!copyValue)
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
await navigator.clipboard.writeText(copyValue);
|
|
15
|
+
showAlert({
|
|
16
|
+
message: t('ActionsMenu.copyToClipboard.success'),
|
|
17
|
+
severity: 'success',
|
|
18
|
+
variant: 'filled'
|
|
19
|
+
});
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
showAlert({
|
|
24
|
+
message: t('ActionsMenu.copyToClipboard.error'),
|
|
25
|
+
severity: 'error',
|
|
26
|
+
variant: 'filled'
|
|
27
|
+
});
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Divider } from '@mui/material';
|
|
2
|
+
import React, { forwardRef } from 'react';
|
|
3
|
+
const DividerAction = forwardRef((_props, ref) => (React.createElement(Divider, { ref: ref, sx: { my: 1 } })));
|
|
4
|
+
DividerAction.displayName = 'divider';
|
|
5
|
+
export const divider = () => {
|
|
6
|
+
return { name: 'divider', Component: DividerAction };
|
|
7
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WebviewService } from 'cozy-intent';
|
|
2
|
+
import { Action, ActionDocument } from '../types';
|
|
3
|
+
export interface DownloadFileOptions<Client> {
|
|
4
|
+
client: Client;
|
|
5
|
+
file: ActionDocument;
|
|
6
|
+
url?: string;
|
|
7
|
+
webviewIntent?: WebviewService;
|
|
8
|
+
driveId?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DownloadActionOptions<Client> {
|
|
11
|
+
client: Client;
|
|
12
|
+
encryptedUrl?: string;
|
|
13
|
+
downloadFile: (options: DownloadFileOptions<Client>) => unknown;
|
|
14
|
+
}
|
|
15
|
+
export declare const download: <Client>({ client, encryptedUrl, downloadFile }: DownloadActionOptions<Client>) => Action;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Download } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const download = ({ client, encryptedUrl, downloadFile }) => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'download',
|
|
8
|
+
label: t('ActionsMenu.download'),
|
|
9
|
+
icon: Download,
|
|
10
|
+
action: (docs, { webviewIntent, driveId }) => downloadFile({
|
|
11
|
+
client,
|
|
12
|
+
file: docs[0],
|
|
13
|
+
url: encryptedUrl,
|
|
14
|
+
webviewIntent,
|
|
15
|
+
driveId
|
|
16
|
+
})
|
|
17
|
+
});
|
|
18
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TextInfo } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const editAttribute = () => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'editAttribute',
|
|
8
|
+
label: t('ActionsMenu.editAttribute'),
|
|
9
|
+
icon: TextInfo,
|
|
10
|
+
action: (docs, { editAttributeCallback }) => editAttributeCallback(docs)
|
|
11
|
+
});
|
|
12
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Email } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const emailTo = () => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'emailTo',
|
|
8
|
+
label: t('ActionsMenu.emailTo'),
|
|
9
|
+
icon: Email,
|
|
10
|
+
action: (docs) => {
|
|
11
|
+
const target = docs[0]?.email?.[0]?.address;
|
|
12
|
+
if (target)
|
|
13
|
+
window.open(`mailto:${target}`, '_self');
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Export } from '@linagora/twake-icons';
|
|
2
|
+
import { downloadBlob } from './helpers';
|
|
3
|
+
import { logger } from './logger';
|
|
4
|
+
import { makeAction } from './makeAction';
|
|
5
|
+
import { makePdfBlobFromText } from './pdfHelpers';
|
|
6
|
+
import { getActionsI18n } from '../locales';
|
|
7
|
+
export const exportToText = ({ exportedText, file }) => {
|
|
8
|
+
const { t } = getActionsI18n();
|
|
9
|
+
return makeAction({
|
|
10
|
+
name: 'exportToText',
|
|
11
|
+
label: t('ActionsMenu.exportToText'),
|
|
12
|
+
icon: Export,
|
|
13
|
+
displayCondition: () => !!exportedText,
|
|
14
|
+
action: async () => {
|
|
15
|
+
try {
|
|
16
|
+
const blob = await makePdfBlobFromText(exportedText ?? '');
|
|
17
|
+
const baseName = file?.name
|
|
18
|
+
? `${file.name.replace(/\.[^/.]+$/, '')}_export`
|
|
19
|
+
: 'export';
|
|
20
|
+
return downloadBlob(blob, `${baseName}.pdf`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
logger.error(error);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Action, ActionDocument, ShowAlert } from '../types';
|
|
2
|
+
export interface FavoritesActionOptions {
|
|
3
|
+
client: {
|
|
4
|
+
save: (doc: ActionDocument) => Promise<unknown>;
|
|
5
|
+
};
|
|
6
|
+
showAlert: ShowAlert;
|
|
7
|
+
splitFilename: (doc: ActionDocument) => {
|
|
8
|
+
filename: string;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare const addToFavorites: (options: FavoritesActionOptions) => Action;
|
|
12
|
+
export declare const removeFromFavorites: (options: FavoritesActionOptions) => Action;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Star, StarOutline } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
const makeFavoriteAction = ({ favorite, client, showAlert, splitFilename }) => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
const scope = favorite ? 'add' : 'remove';
|
|
7
|
+
return makeAction({
|
|
8
|
+
name: favorite ? 'addToFavorites' : 'removeFromFavorites',
|
|
9
|
+
label: t(`ActionsMenu.favorites.${scope}.label`),
|
|
10
|
+
icon: favorite ? StarOutline : Star,
|
|
11
|
+
displayCondition: docs => docs.length > 0 &&
|
|
12
|
+
docs.every(doc => !!doc.cozyMetadata?.favorite !== favorite),
|
|
13
|
+
action: async (docs) => {
|
|
14
|
+
try {
|
|
15
|
+
for (const doc of docs) {
|
|
16
|
+
await client.save({
|
|
17
|
+
...doc,
|
|
18
|
+
cozyMetadata: { ...doc.cozyMetadata, favorite }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
const { filename } = splitFilename(docs[0]);
|
|
22
|
+
showAlert({
|
|
23
|
+
message: t(`ActionsMenu.favorites.${scope}.success`, {
|
|
24
|
+
filename,
|
|
25
|
+
smart_count: docs.length
|
|
26
|
+
}),
|
|
27
|
+
severity: 'success'
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
showAlert({
|
|
32
|
+
message: t('ActionsMenu.favorites.error'),
|
|
33
|
+
severity: 'error'
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
export const addToFavorites = (options) => {
|
|
40
|
+
return makeFavoriteAction({ ...options, favorite: true });
|
|
41
|
+
};
|
|
42
|
+
export const removeFromFavorites = (options) => {
|
|
43
|
+
return makeFavoriteAction({ ...options, favorite: false });
|
|
44
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PDFDocument } from 'pdf-lib';
|
|
2
|
+
import { ActionDocument, ActionFactory, ActionObject, GenerateWebLink, WebLinkClient } from '../types';
|
|
3
|
+
/** Builds every action with the same options, skipping falsy entries */
|
|
4
|
+
export declare const makeActions: <Options extends object>(actions?: (ActionFactory<Options> | false | null | undefined)[], options?: Options) => ActionObject[];
|
|
5
|
+
export interface AppWebLinkOptions {
|
|
6
|
+
client: WebLinkClient;
|
|
7
|
+
generateWebLink: GenerateWebLink;
|
|
8
|
+
slug: string;
|
|
9
|
+
hash: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const makeAppWebLink: ({ client, generateWebLink, slug, hash }: AppWebLinkOptions) => string;
|
|
12
|
+
export declare const makeBase64FromFile: (file: Blob) => Promise<string | null>;
|
|
13
|
+
export declare const fileToArrayBuffer: (file: Blob) => Promise<ArrayBuffer | Uint8Array>;
|
|
14
|
+
/** Adds a PDF or an image file to the document, and returns its bytes */
|
|
15
|
+
export declare const addFileToPdf: (pdfDoc: PDFDocument, file: Blob) => Promise<Uint8Array>;
|
|
16
|
+
export type FetchBlobFileById<Client> = (client: Client, id: string) => Promise<Blob>;
|
|
17
|
+
export interface MakePdfBlobOptions<Client> {
|
|
18
|
+
client: Client;
|
|
19
|
+
docs: ActionDocument[];
|
|
20
|
+
fetchBlobFileById: FetchBlobFileById<Client>;
|
|
21
|
+
}
|
|
22
|
+
/** Fetches the files of the docs and merges them into a single PDF */
|
|
23
|
+
export declare const makePdfBlob: <Client>({ client, docs, fetchBlobFileById }: MakePdfBlobOptions<Client>) => Promise<Blob>;
|
|
24
|
+
export declare const downloadBlob: (blob: Blob | null, filename: string) => boolean;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Should guarantee good resolution for different uses (printing, downloading, etc.)
|
|
2
|
+
const MAX_RESIZE_IMAGE_SIZE = 3840;
|
|
3
|
+
const MAX_IMAGE_SIDE_SIZE = 1920;
|
|
4
|
+
/** Builds every action with the same options, skipping falsy entries */
|
|
5
|
+
export const makeActions = (actions = [], options) => {
|
|
6
|
+
// SAFETY: a factory that needs options reads them as its own contract
|
|
7
|
+
const factoryOptions = options ?? {};
|
|
8
|
+
return actions
|
|
9
|
+
.filter((actionFn) => !!actionFn)
|
|
10
|
+
.map(actionFn => {
|
|
11
|
+
const action = actionFn(factoryOptions);
|
|
12
|
+
const name = action.name || actionFn.name;
|
|
13
|
+
return { [name]: action };
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
export const makeAppWebLink = ({ client, generateWebLink, slug, hash }) => {
|
|
17
|
+
return generateWebLink({
|
|
18
|
+
slug,
|
|
19
|
+
cozyUrl: client.getStackClient().uri,
|
|
20
|
+
subDomainType: client.getInstanceOptions().subdomain,
|
|
21
|
+
pathname: '/',
|
|
22
|
+
hash
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
const readFileAs = (file, method) => {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const reader = new FileReader();
|
|
28
|
+
reader.onerror = reject;
|
|
29
|
+
reader.onload = () => resolve(reader.result);
|
|
30
|
+
reader[method](file);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
export const makeBase64FromFile = async (file) => {
|
|
34
|
+
const result = await readFileAs(file, 'readAsDataURL');
|
|
35
|
+
return typeof result === 'string' ? result : null;
|
|
36
|
+
};
|
|
37
|
+
const computeImageScaleRatio = (image) => {
|
|
38
|
+
const longerSideSizeInPixel = Math.max(image.height, image.width);
|
|
39
|
+
return MAX_RESIZE_IMAGE_SIZE < longerSideSizeInPixel
|
|
40
|
+
? MAX_RESIZE_IMAGE_SIZE / longerSideSizeInPixel
|
|
41
|
+
: 1;
|
|
42
|
+
};
|
|
43
|
+
const resizeImage = (fileDataUri) => {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
const image = new Image();
|
|
46
|
+
image.src = fileDataUri;
|
|
47
|
+
image.onerror = reject;
|
|
48
|
+
image.onload = () => {
|
|
49
|
+
const canvas = document.createElement('canvas');
|
|
50
|
+
const scaleRatio = computeImageScaleRatio(image);
|
|
51
|
+
const scaledWidth = scaleRatio * image.width;
|
|
52
|
+
const scaledHeight = scaleRatio * image.height;
|
|
53
|
+
const quality = scaledWidth >= MAX_IMAGE_SIDE_SIZE ||
|
|
54
|
+
scaledHeight >= MAX_IMAGE_SIDE_SIZE
|
|
55
|
+
? 0.35
|
|
56
|
+
: 0.75;
|
|
57
|
+
canvas.width = scaledWidth;
|
|
58
|
+
canvas.height = scaledHeight;
|
|
59
|
+
canvas.getContext('2d')?.drawImage(image, 0, 0, scaledWidth, scaledHeight);
|
|
60
|
+
resolve(canvas.toDataURL('image/jpeg', quality));
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const addImageToPdf = async (pdfDoc, file) => {
|
|
65
|
+
const fileDataUri = await makeBase64FromFile(file);
|
|
66
|
+
if (fileDataUri === null)
|
|
67
|
+
throw new Error('Cannot read the image file');
|
|
68
|
+
const resizedImage = await resizeImage(fileDataUri);
|
|
69
|
+
const img = await pdfDoc.embedJpg(resizedImage);
|
|
70
|
+
const page = pdfDoc.addPage([img.width, img.height]);
|
|
71
|
+
const { width: pageWidth, height: pageHeight } = page.getSize();
|
|
72
|
+
page.drawImage(img, {
|
|
73
|
+
x: pageWidth / 2 - img.width / 2,
|
|
74
|
+
y: pageHeight / 2 - img.height / 2,
|
|
75
|
+
width: img.width,
|
|
76
|
+
height: img.height
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
export const fileToArrayBuffer = async (file) => {
|
|
80
|
+
if ('arrayBuffer' in file)
|
|
81
|
+
return await file.arrayBuffer();
|
|
82
|
+
const result = await readFileAs(file, 'readAsArrayBuffer');
|
|
83
|
+
if (!(result instanceof ArrayBuffer)) {
|
|
84
|
+
throw new Error('Cannot read the file');
|
|
85
|
+
}
|
|
86
|
+
return new Uint8Array(result);
|
|
87
|
+
};
|
|
88
|
+
const addPdfToPdf = async (pdfDoc, file) => {
|
|
89
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
90
|
+
const pdfToAdd = await fileToArrayBuffer(file);
|
|
91
|
+
const document = await PDFDocument.load(pdfToAdd);
|
|
92
|
+
const copiedPages = await pdfDoc.copyPages(document, document.getPageIndices());
|
|
93
|
+
copiedPages.forEach(page => pdfDoc.addPage(page));
|
|
94
|
+
};
|
|
95
|
+
/** Adds a PDF or an image file to the document, and returns its bytes */
|
|
96
|
+
export const addFileToPdf = async (pdfDoc, file) => {
|
|
97
|
+
if (file.type === 'application/pdf') {
|
|
98
|
+
await addPdfToPdf(pdfDoc, file);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
await addImageToPdf(pdfDoc, file);
|
|
102
|
+
}
|
|
103
|
+
return await pdfDoc.save();
|
|
104
|
+
};
|
|
105
|
+
/** Fetches the files of the docs and merges them into a single PDF */
|
|
106
|
+
export const makePdfBlob = async ({ client, docs, fetchBlobFileById }) => {
|
|
107
|
+
const { PDFDocument } = await import('pdf-lib');
|
|
108
|
+
const pdfDoc = await PDFDocument.create();
|
|
109
|
+
for (const doc of docs) {
|
|
110
|
+
const blob = await fetchBlobFileById(client, doc._id);
|
|
111
|
+
await addFileToPdf(pdfDoc, blob);
|
|
112
|
+
}
|
|
113
|
+
const pdfBytes = await pdfDoc.save();
|
|
114
|
+
return new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
|
115
|
+
};
|
|
116
|
+
export const downloadBlob = (blob, filename) => {
|
|
117
|
+
if (!blob || !filename)
|
|
118
|
+
return false;
|
|
119
|
+
const url = URL.createObjectURL(blob);
|
|
120
|
+
const a = document.createElement('a');
|
|
121
|
+
a.href = url;
|
|
122
|
+
a.download = filename;
|
|
123
|
+
a.style.display = 'none';
|
|
124
|
+
document.body.appendChild(a);
|
|
125
|
+
a.click();
|
|
126
|
+
a.remove();
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Action, ActionComponent, ActionComponentSlotProps } from '../types';
|
|
2
|
+
export interface MakeActionComponentOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
icon?: Action['icon'];
|
|
6
|
+
componentProps?: ActionComponentSlotProps;
|
|
7
|
+
}
|
|
8
|
+
export declare const makeActionComponent: ({ name, label, icon, componentProps }: MakeActionComponentOptions) => ActionComponent;
|
|
9
|
+
export type MakeActionOptions = Omit<Action, 'Component'> & Partial<Pick<Action, 'Component'>>;
|
|
10
|
+
/** Makes an action, with a default `Component` when none is given */
|
|
11
|
+
export declare const makeAction: ({ Component, ...action }: MakeActionOptions) => Action;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Icon } from '@linagora/twake-icons';
|
|
2
|
+
import { ListItemIcon } from '@mui/material';
|
|
3
|
+
import React, { forwardRef } from 'react';
|
|
4
|
+
import ListItemText from '../../ListItemText';
|
|
5
|
+
import ActionsMenuItem from '../ActionsMenuItem';
|
|
6
|
+
export const makeActionComponent = ({ name, label, icon, componentProps }) => {
|
|
7
|
+
const Component = forwardRef((props, ref) => (React.createElement(ActionsMenuItem, { ...props, ref: ref },
|
|
8
|
+
React.createElement(ListItemIcon, null, icon !== undefined && React.createElement(Icon, { ...componentProps?.Icon, icon: icon })),
|
|
9
|
+
React.createElement(ListItemText, { primary: label, ...componentProps?.ListItemText }))));
|
|
10
|
+
Component.displayName = name;
|
|
11
|
+
return Component;
|
|
12
|
+
};
|
|
13
|
+
/** Makes an action, with a default `Component` when none is given */
|
|
14
|
+
export const makeAction = ({ Component, ...action }) => {
|
|
15
|
+
return {
|
|
16
|
+
...action,
|
|
17
|
+
Component: Component ?? makeActionComponent(action)
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Pen } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAppWebLink } from './helpers';
|
|
3
|
+
import { makeAction } from './makeAction';
|
|
4
|
+
import { getActionsI18n } from '../locales';
|
|
5
|
+
export const modify = ({ client, generateWebLink }) => {
|
|
6
|
+
const { t } = getActionsI18n();
|
|
7
|
+
return makeAction({
|
|
8
|
+
name: 'modify',
|
|
9
|
+
label: t('ActionsMenu.modify'),
|
|
10
|
+
icon: Pen,
|
|
11
|
+
action: (docs) => {
|
|
12
|
+
const webLink = makeAppWebLink({
|
|
13
|
+
client,
|
|
14
|
+
generateWebLink,
|
|
15
|
+
slug: 'contacts',
|
|
16
|
+
hash: `/${docs[0]._id}/edit`
|
|
17
|
+
});
|
|
18
|
+
window.open(webLink, '_blank');
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Dots } from '@linagora/twake-icons';
|
|
2
|
+
import { makeAction } from './makeAction';
|
|
3
|
+
import { getActionsI18n } from '../locales';
|
|
4
|
+
export const others = () => {
|
|
5
|
+
const { t } = getActionsI18n();
|
|
6
|
+
return makeAction({
|
|
7
|
+
name: 'others',
|
|
8
|
+
label: t('ActionsMenu.others'),
|
|
9
|
+
icon: Dots
|
|
10
|
+
});
|
|
11
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PDFDocument, PDFFont, PDFPage } from 'pdf-lib';
|
|
2
|
+
export interface WrapTextToLinesOptions {
|
|
3
|
+
font: PDFFont;
|
|
4
|
+
fontSize: number;
|
|
5
|
+
/** Max line width, in PDF units */
|
|
6
|
+
maxLineWidth: number;
|
|
7
|
+
line: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const wrapTextToLines: ({ font, fontSize, maxLineWidth, line }: WrapTextToLinesOptions) => string[];
|
|
10
|
+
export interface PdfCursor {
|
|
11
|
+
page: PDFPage;
|
|
12
|
+
y: number;
|
|
13
|
+
}
|
|
14
|
+
export interface PushNewPageIfNeededOptions extends PdfCursor {
|
|
15
|
+
pdfDoc: PDFDocument;
|
|
16
|
+
requiredHeight: number;
|
|
17
|
+
marginTop: number;
|
|
18
|
+
marginBottom: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const pushNewPageIfNeeded: ({ pdfDoc, page, y, requiredHeight, marginTop, marginBottom }: PushNewPageIfNeededOptions) => PdfCursor;
|
|
21
|
+
export interface DrawWrappedTextOptions extends PdfCursor {
|
|
22
|
+
pdfDoc: PDFDocument;
|
|
23
|
+
text: string;
|
|
24
|
+
font: PDFFont;
|
|
25
|
+
fontSize: number;
|
|
26
|
+
lineHeight: number;
|
|
27
|
+
marginX: number;
|
|
28
|
+
/** Top and bottom margin */
|
|
29
|
+
marginY: number;
|
|
30
|
+
/** Max line width, in PDF units */
|
|
31
|
+
maxLineWidth: number;
|
|
32
|
+
}
|
|
33
|
+
export declare const drawWrappedText: ({ pdfDoc, page, y, text, font, fontSize, lineHeight, marginX, marginY, maxLineWidth }: DrawWrappedTextOptions) => PdfCursor;
|
|
34
|
+
/** Creates an A4 PDF from a plain text */
|
|
35
|
+
export declare const makePdfBlobFromText: (text: string) => Promise<Blob>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const A4_PAGE_SIZE = [595.28, 841.89];
|
|
2
|
+
const TEXT_PDF_CONFIG = {
|
|
3
|
+
fontSize: 12,
|
|
4
|
+
lineHeight: 16,
|
|
5
|
+
marginX: 40,
|
|
6
|
+
marginY: 40,
|
|
7
|
+
pageSize: A4_PAGE_SIZE
|
|
8
|
+
};
|
|
9
|
+
export const wrapTextToLines = ({ font, fontSize, maxLineWidth, line }) => {
|
|
10
|
+
if (!line)
|
|
11
|
+
return [];
|
|
12
|
+
if (fontSize <= 0 || maxLineWidth <= 0)
|
|
13
|
+
return [line];
|
|
14
|
+
return line.split(' ').reduce((lines, word) => {
|
|
15
|
+
const currentLine = lines.at(-1);
|
|
16
|
+
if (currentLine === undefined)
|
|
17
|
+
return [word];
|
|
18
|
+
const testLine = `${currentLine} ${word}`;
|
|
19
|
+
if (font.widthOfTextAtSize(testLine, fontSize) <= maxLineWidth) {
|
|
20
|
+
lines[lines.length - 1] = testLine;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
lines.push(word);
|
|
24
|
+
}
|
|
25
|
+
return lines;
|
|
26
|
+
}, []);
|
|
27
|
+
};
|
|
28
|
+
export const pushNewPageIfNeeded = ({ pdfDoc, page, y, requiredHeight, marginTop, marginBottom }) => {
|
|
29
|
+
if (y - requiredHeight >= marginBottom)
|
|
30
|
+
return { page, y };
|
|
31
|
+
const { width, height } = page.getSize();
|
|
32
|
+
return {
|
|
33
|
+
page: pdfDoc.addPage([width, height]),
|
|
34
|
+
y: height - marginTop
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export const drawWrappedText = ({ pdfDoc, page, y, text, font, fontSize, lineHeight, marginX, marginY, maxLineWidth }) => {
|
|
38
|
+
let cursor = { page, y };
|
|
39
|
+
for (const paragraph of text.split(/\r?\n/)) {
|
|
40
|
+
for (const line of wrapTextToLines({
|
|
41
|
+
font,
|
|
42
|
+
fontSize,
|
|
43
|
+
maxLineWidth,
|
|
44
|
+
line: paragraph
|
|
45
|
+
})) {
|
|
46
|
+
cursor = pushNewPageIfNeeded({
|
|
47
|
+
pdfDoc,
|
|
48
|
+
...cursor,
|
|
49
|
+
requiredHeight: lineHeight,
|
|
50
|
+
marginTop: marginY,
|
|
51
|
+
marginBottom: marginY
|
|
52
|
+
});
|
|
53
|
+
cursor.page.drawText(line, {
|
|
54
|
+
x: marginX,
|
|
55
|
+
y: cursor.y - fontSize,
|
|
56
|
+
size: fontSize,
|
|
57
|
+
font
|
|
58
|
+
});
|
|
59
|
+
cursor = { ...cursor, y: cursor.y - lineHeight };
|
|
60
|
+
}
|
|
61
|
+
cursor = { ...cursor, y: cursor.y - lineHeight / 2 };
|
|
62
|
+
}
|
|
63
|
+
return cursor;
|
|
64
|
+
};
|
|
65
|
+
/** Creates an A4 PDF from a plain text */
|
|
66
|
+
export const makePdfBlobFromText = async (text) => {
|
|
67
|
+
const { PDFDocument, StandardFonts } = await import('pdf-lib');
|
|
68
|
+
const pdfDoc = await PDFDocument.create();
|
|
69
|
+
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
70
|
+
const { fontSize, lineHeight, marginX, marginY, pageSize } = TEXT_PDF_CONFIG;
|
|
71
|
+
drawWrappedText({
|
|
72
|
+
pdfDoc,
|
|
73
|
+
page: pdfDoc.addPage(pageSize),
|
|
74
|
+
y: pageSize[1] - marginY,
|
|
75
|
+
text,
|
|
76
|
+
font,
|
|
77
|
+
fontSize,
|
|
78
|
+
lineHeight,
|
|
79
|
+
marginX,
|
|
80
|
+
marginY,
|
|
81
|
+
maxLineWidth: pageSize[0] - marginX * 2
|
|
82
|
+
});
|
|
83
|
+
const pdfBytes = await pdfDoc.save();
|
|
84
|
+
return new Blob([new Uint8Array(pdfBytes)], { type: 'application/pdf' });
|
|
85
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { FetchBlobFileById } from './helpers';
|
|
2
|
+
import { Action, ActionDocument } from '../types';
|
|
3
|
+
export interface PrintClient {
|
|
4
|
+
collection: (doctype: 'io.cozy.files') => {
|
|
5
|
+
getDownloadLinkById: (id: string, filename?: string) => Promise<string>;
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export interface PrintActionOptions<Client extends PrintClient> {
|
|
9
|
+
client: Client;
|
|
10
|
+
fetchBlobFileById: FetchBlobFileById<Client>;
|
|
11
|
+
isFile: (doc: ActionDocument) => boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const print: <Client extends PrintClient>({ client, fetchBlobFileById, isFile }: PrintActionOptions<Client>) => Action;
|