@evoke-platform/ui-components 1.6.0-dev.18 → 1.6.0-dev.19

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.
@@ -1,9 +1,9 @@
1
+ /// <reference types="react" />
1
2
  import { Obj } from '@evoke-platform/context';
2
- import { Dispatch, SetStateAction } from 'react';
3
3
  import { FieldValues, UseFormGetValues } from 'react-hook-form';
4
4
  type FormContextType = {
5
5
  fetchedOptions: FieldValues;
6
- setFetchedOptions: Dispatch<SetStateAction<FieldValues>>;
6
+ setFetchedOptions: (newData: FieldValues) => void;
7
7
  getValues: UseFormGetValues<FieldValues>;
8
8
  stickyFooter?: boolean;
9
9
  object?: Obj;
@@ -42,13 +42,12 @@ export default function Criteria(props) {
42
42
  return prop;
43
43
  });
44
44
  setProperties(flattenProperties);
45
- setFetchedOptions((prev) => ({
46
- ...prev,
45
+ setFetchedOptions({
47
46
  [`${fieldDefinition.id}Options`]: flattenProperties.map((prop) => ({
48
47
  id: prop.id,
49
48
  name: prop.name,
50
49
  })),
51
- }));
50
+ });
52
51
  setLoadingError(false);
53
52
  }
54
53
  setLoading(false);
@@ -0,0 +1,16 @@
1
+ import { DocumentValidation } from '@evoke-platform/context';
2
+ import React from 'react';
3
+ import { FieldValues } from 'react-hook-form';
4
+ import { SavedDocumentReference } from '../../types';
5
+ type DocumentProps = {
6
+ id: string;
7
+ handleChange: (propertyId: string, value: (File | SavedDocumentReference)[] | undefined) => void;
8
+ instance?: FieldValues;
9
+ canUpdateProperty: boolean;
10
+ error: boolean;
11
+ validate?: DocumentValidation;
12
+ value: (File | SavedDocumentReference)[] | undefined;
13
+ hasDescription?: boolean;
14
+ };
15
+ export declare const Document: (props: DocumentProps) => React.JSX.Element;
16
+ export {};
@@ -0,0 +1,73 @@
1
+ import { useApiServices } from '@evoke-platform/context';
2
+ import { isNil } from 'lodash';
3
+ import React, { useEffect, useState } from 'react';
4
+ import { useDropzone } from 'react-dropzone';
5
+ import { UploadCloud } from '../../../../../../icons';
6
+ import { useFormContext } from '../../../../../../theme/hooks';
7
+ import { Skeleton, Snackbar, Typography } from '../../../../../core';
8
+ import { Box, Grid } from '../../../../../layout';
9
+ import { getPrefixedUrl } from '../../utils';
10
+ import { DocumentList } from './DocumentList';
11
+ export const Document = (props) => {
12
+ const { id, handleChange, canUpdateProperty, error, instance, value, validate, hasDescription } = props;
13
+ const apiServices = useApiServices();
14
+ const { fetchedOptions, setFetchedOptions, object } = useFormContext();
15
+ const [snackbarError, setSnackbarError] = useState();
16
+ const [documents, setDocuments] = useState();
17
+ const [hasUpdatePermission, setHasUpdatePermission] = useState(fetchedOptions[`${id}UpdatePermission`]);
18
+ useEffect(() => {
19
+ setDocuments(value);
20
+ }, [value]);
21
+ useEffect(() => {
22
+ checkPermissions();
23
+ }, [object]);
24
+ const checkPermissions = () => {
25
+ if (canUpdateProperty && !fetchedOptions[`${id}UpdatePermission`]) {
26
+ apiServices
27
+ .get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=update`))
28
+ .then((accessCheck) => {
29
+ setFetchedOptions({
30
+ [`${id}UpdatePermission`]: accessCheck.result,
31
+ });
32
+ setHasUpdatePermission(accessCheck.result);
33
+ });
34
+ }
35
+ };
36
+ const handleUpload = async (files) => {
37
+ const newDocuments = [...(documents ?? []), ...(files ?? [])];
38
+ setDocuments(newDocuments);
39
+ handleChange(id, newDocuments);
40
+ };
41
+ const uploadDisabled = !!validate?.maxDocuments && (documents?.length ?? 0) >= validate.maxDocuments;
42
+ const { getRootProps, getInputProps, open } = useDropzone({
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ onDrop: (files) => handleUpload(files),
45
+ disabled: uploadDisabled,
46
+ });
47
+ return (React.createElement(React.Fragment, null,
48
+ canUpdateProperty && hasUpdatePermission && (React.createElement(Box, { sx: {
49
+ margin: '5px 0',
50
+ height: '115px',
51
+ borderRadius: '8px',
52
+ display: 'flex',
53
+ justifyContent: 'center',
54
+ alignItems: 'center',
55
+ border: `1px dashed ${error ? 'red' : uploadDisabled ? '#DFE3E8' : '#858585'}`,
56
+ position: 'relative',
57
+ cursor: uploadDisabled ? 'cursor' : 'pointer',
58
+ }, ...getRootProps(), onClick: open },
59
+ React.createElement("input", { ...getInputProps({ id }), disabled: uploadDisabled, ...(hasDescription ? { 'aria-describedby': `${id}-description` } : undefined) }),
60
+ React.createElement(Grid, { container: true, sx: { width: '100%' } },
61
+ React.createElement(Grid, { item: true, xs: 12, sx: { display: 'flex', justifyContent: 'center', paddingBottom: '7px' } },
62
+ React.createElement(UploadCloud, { sx: { color: '#919EAB', width: '50px', height: '30px' } })),
63
+ React.createElement(Grid, { item: true, xs: 12 },
64
+ React.createElement(Typography, { variant: "body2", sx: { color: uploadDisabled ? '#919EAB' : '#212B36', textAlign: 'center' } },
65
+ "Drag and drop or",
66
+ ' ',
67
+ React.createElement(Typography, { component: 'span', color: uploadDisabled ? '#919EAB' : 'primary', sx: { fontSize: '14px' } }, "select file"),
68
+ ' ',
69
+ "to upload"))))),
70
+ canUpdateProperty && isNil(hasUpdatePermission) && (React.createElement(Skeleton, { variant: "rectangular", height: "115px", sx: { margin: '5px 0', borderRadius: '8px' } })),
71
+ React.createElement(DocumentList, { id: id, instance: instance, handleChange: handleChange, value: value, setSnackbarError: (type, message) => setSnackbarError({ message, type }), canUpdateProperty: canUpdateProperty && !!hasUpdatePermission }),
72
+ React.createElement(Snackbar, { open: !!snackbarError?.message, handleClose: () => setSnackbarError(null), message: snackbarError?.message, error: snackbarError?.type === 'error' })));
73
+ };
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { FieldValues } from 'react-hook-form';
3
+ import { SavedDocumentReference } from '../../types';
4
+ type DocumentListProps = {
5
+ handleChange: (propertyId: string, value: (File | SavedDocumentReference)[] | undefined) => void;
6
+ id: string;
7
+ instance?: FieldValues;
8
+ canUpdateProperty: boolean;
9
+ value: (File | SavedDocumentReference)[] | undefined;
10
+ setSnackbarError: (type: 'error' | 'success', message: string) => void;
11
+ };
12
+ export declare const DocumentList: (props: DocumentListProps) => React.JSX.Element;
13
+ export {};
@@ -0,0 +1,179 @@
1
+ import { useApiServices } from '@evoke-platform/context';
2
+ import { isEqual } from 'lodash';
3
+ import prettyBytes from 'pretty-bytes';
4
+ import React, { useEffect, useState } from 'react';
5
+ import { FileWithExtension, LaunchRounded, TrashCan, WarningRounded } from '../../../../../../icons';
6
+ import { useFormContext } from '../../../../../../theme/hooks';
7
+ import { Chip, IconButton, Typography } from '../../../../../core';
8
+ import { Box, Grid } from '../../../../../layout';
9
+ import { getPrefixedUrl } from '../../utils';
10
+ const styles = {
11
+ icon: {
12
+ padding: '3px',
13
+ color: '#637381',
14
+ },
15
+ };
16
+ const viewableFileTypes = [
17
+ 'application/pdf',
18
+ 'image/jpeg',
19
+ 'image/jpg',
20
+ 'image/png',
21
+ 'image/gif',
22
+ 'image/bmp',
23
+ 'image/webp',
24
+ 'text/plain',
25
+ ];
26
+ export const DocumentList = (props) => {
27
+ const { handleChange, id, canUpdateProperty, instance, value: documents, setSnackbarError } = props;
28
+ const apiServices = useApiServices();
29
+ const { fetchedOptions, setFetchedOptions, object } = useFormContext();
30
+ const [hasViewPermission, setHasViewPermission] = useState(fetchedOptions[`${id}ViewPermission`] ?? true);
31
+ const [savedDocuments, setSavedDocuments] = useState(fetchedOptions[`${id}SavedDocuments`]);
32
+ useEffect(() => {
33
+ const currentValue = instance?.[id];
34
+ if (currentValue?.length) {
35
+ const currentDocumentIds = currentValue.map((doc) => doc.id);
36
+ if (currentDocumentIds.length &&
37
+ // these need to be sorted otherwise it will evaluate as not equal if the ids are in different orders causing unnecessary fetches
38
+ !isEqual(currentDocumentIds.slice().sort(), savedDocuments
39
+ ?.map((doc) => doc.id)
40
+ .slice()
41
+ .sort())) {
42
+ getDocuments(currentDocumentIds);
43
+ }
44
+ }
45
+ }, [id, documents, object]);
46
+ useEffect(() => {
47
+ if (fetchedOptions[`${id}SavedDocuments`]) {
48
+ setSavedDocuments(fetchedOptions[`${id}SavedDocuments`]);
49
+ }
50
+ }, [fetchedOptions]);
51
+ const getDocuments = (currentDocumentIds, shouldRetry = true) => {
52
+ apiServices.get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents`), {
53
+ params: { filter: { where: { id: { inq: currentDocumentIds } } } },
54
+ }, (error, docs) => {
55
+ // There is a short delay between when a document is uploaded and when
56
+ // it is indexed. Therefore, try again if documents are not found.
57
+ if (shouldRetry &&
58
+ (!docs || currentDocumentIds.some((docId) => !docs.find((doc) => docId === doc.id)))) {
59
+ setTimeout(() => getDocuments(currentDocumentIds, false), 2000);
60
+ }
61
+ else if (error) {
62
+ setSnackbarError('error', 'Error occurred while retrieving saved documents');
63
+ }
64
+ else {
65
+ setSavedDocuments(docs);
66
+ setFetchedOptions({
67
+ [`${id}SavedDocuments`]: docs,
68
+ });
69
+ }
70
+ });
71
+ };
72
+ useEffect(() => {
73
+ if (!fetchedOptions[`${id}ViewPermission`]) {
74
+ checkPermissions();
75
+ }
76
+ }, [object]);
77
+ const checkPermissions = () => {
78
+ if (instance?.[id]?.length) {
79
+ apiServices
80
+ .get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/checkAccess?action=view`))
81
+ .then((viewPermissionCheck) => {
82
+ setFetchedOptions({
83
+ [`${id}ViewPermission`]: viewPermissionCheck.result,
84
+ });
85
+ setHasViewPermission(viewPermissionCheck.result);
86
+ });
87
+ }
88
+ };
89
+ const isFile = (doc) => doc instanceof File;
90
+ const fileExists = (doc) => savedDocuments?.find((d) => d.id === doc.id);
91
+ const handleRemove = (index) => {
92
+ const updatedDocuments = documents?.filter((_, i) => i !== index) ?? [];
93
+ handleChange(id, updatedDocuments.length === 0 ? undefined : updatedDocuments);
94
+ };
95
+ const openDocument = async (index) => {
96
+ const doc = documents?.[index];
97
+ if (doc) {
98
+ let url;
99
+ const contentType = doc instanceof File
100
+ ? doc.type
101
+ : savedDocuments?.find((savedDocument) => savedDocument.id === doc.id)?.contentType;
102
+ if (!isFile(doc)) {
103
+ try {
104
+ const documentResponse = await apiServices.get(getPrefixedUrl(`/objects/${object?.id}/instances/${instance?.id}/documents/${doc.id}/content`), { responseType: 'blob' });
105
+ const blob = new Blob([documentResponse], { type: contentType });
106
+ url = window.URL.createObjectURL(blob);
107
+ }
108
+ catch (error) {
109
+ setSnackbarError('error', `Could not open ${doc.name}`);
110
+ return;
111
+ }
112
+ }
113
+ else {
114
+ url = URL.createObjectURL(doc);
115
+ }
116
+ if (contentType && viewableFileTypes.includes(contentType)) {
117
+ window.open(url, '_blank');
118
+ }
119
+ else {
120
+ const link = document.createElement('a');
121
+ link.href = url;
122
+ link.setAttribute('download', doc.name);
123
+ document.body.appendChild(link);
124
+ link.click();
125
+ // Clean up and remove the link
126
+ link.parentNode?.removeChild(link);
127
+ }
128
+ }
129
+ };
130
+ const getDocumentSize = (doc) => {
131
+ let size;
132
+ if (isFile(doc)) {
133
+ size = prettyBytes(doc.size);
134
+ }
135
+ else {
136
+ const savedDoc = savedDocuments?.find((savedDocument) => savedDocument.id === doc.id);
137
+ size = savedDoc ? prettyBytes(savedDoc.size) : '';
138
+ }
139
+ return size;
140
+ };
141
+ return (React.createElement(React.Fragment, null,
142
+ !documents && !canUpdateProperty && (React.createElement(Typography, { variant: "body2", sx: { color: '#637381' } }, "No documents")),
143
+ !!documents?.length && (React.createElement(Box, null, documents.map((doc, index) => (React.createElement(Grid, { container: true, sx: {
144
+ width: '100%',
145
+ border: '1px solid #C4CDD5',
146
+ borderRadius: '6px',
147
+ margin: '5px 2px',
148
+ padding: ' 8px',
149
+ display: 'flex',
150
+ alignItems: 'center',
151
+ } },
152
+ React.createElement(Grid, { item: true, sx: { display: 'flex', justifyContent: 'center', padding: '7px', marginLeft: '4px' } },
153
+ React.createElement(FileWithExtension, { fontFamily: "Arial", fileExtension: doc.name?.split('.')?.pop() ?? '', sx: { height: '1.5em', width: '1.5em' } })),
154
+ React.createElement(Grid, { item: true, sx: { flex: 1, justifyContent: 'center', paddingBottom: '5px' } },
155
+ React.createElement(Grid, { item: true, xs: 12 },
156
+ React.createElement(Typography, { sx: {
157
+ fontSize: '14px',
158
+ fontWeight: 700,
159
+ lineHeight: '15px',
160
+ paddingTop: '8px',
161
+ } }, doc.name)),
162
+ React.createElement(Grid, { item: true, xs: 12 },
163
+ React.createElement(Typography, { sx: { fontSize: '12px', color: '#637381' } }, getDocumentSize(doc)))),
164
+ (isFile(doc) || (hasViewPermission && !isFile(doc) && fileExists(doc))) && (React.createElement(Grid, { item: true },
165
+ React.createElement(IconButton, { "aria-label": "open document", sx: { ...styles.icon, marginRight: '16px' }, onClick: () => openDocument(index) },
166
+ React.createElement(LaunchRounded, { sx: { color: '#637381', fontSize: '22px' } })))),
167
+ !isFile(doc) && savedDocuments && !fileExists(doc) && (React.createElement(Chip, { label: "Deleted", sx: {
168
+ marginRight: '16px',
169
+ backgroundColor: 'rgba(222, 48, 36, 0.16)',
170
+ color: '#A91813',
171
+ borderRadius: '6px',
172
+ height: '25px',
173
+ fontWeight: 700,
174
+ '& .MuiChip-icon': { color: '#A91813', width: '.8em', marginBottom: '2px' },
175
+ }, icon: React.createElement(WarningRounded, null) })),
176
+ canUpdateProperty && (React.createElement(Grid, { item: true },
177
+ React.createElement(IconButton, { "aria-label": "delete document", sx: styles.icon, onClick: () => handleRemove(index) },
178
+ React.createElement(TrashCan, { sx: { ':hover': { color: '#A12723' } } })))))))))));
179
+ };
@@ -30,13 +30,12 @@ const UserProperty = (props) => {
30
30
  label: user.name,
31
31
  value: user.id,
32
32
  })));
33
- setFetchedOptions((prev) => ({
34
- ...prev,
33
+ setFetchedOptions({
35
34
  [`${id}Options`]: (userList ?? []).map((user) => ({
36
35
  label: user.name,
37
36
  value: user.id,
38
37
  })),
39
- }));
38
+ });
40
39
  setLoadingOptions(false);
41
40
  });
42
41
  }
@@ -6,3 +6,21 @@ export type FieldAddress = {
6
6
  state?: string;
7
7
  zipCode?: string;
8
8
  };
9
+ export type AccessCheck = {
10
+ result: boolean;
11
+ };
12
+ export type SavedDocumentReference = {
13
+ id: string;
14
+ name: string;
15
+ };
16
+ export type Document = {
17
+ id: string;
18
+ name: string;
19
+ contentType: string;
20
+ size: number;
21
+ uploadedDate: string;
22
+ metadata: {
23
+ [field: string]: unknown;
24
+ };
25
+ versionId?: string;
26
+ };
@@ -1,4 +1,3 @@
1
- /// <reference types="react" />
2
1
  import { Breakpoint } from '@mui/material/styles';
3
2
  /**
4
3
  * Custom hook for responsive design breakpoints using size terminology.
@@ -110,7 +109,7 @@ export declare const useWidgetSize: (options?: {
110
109
  export default useWidgetSize;
111
110
  export declare function useFormContext(): {
112
111
  fetchedOptions: import("react-hook-form").FieldValues;
113
- setFetchedOptions: import("react").Dispatch<import("react").SetStateAction<import("react-hook-form").FieldValues>>;
112
+ setFetchedOptions: (newData: import("react-hook-form").FieldValues) => void;
114
113
  getValues: import("react-hook-form").UseFormGetValues<import("react-hook-form").FieldValues>;
115
114
  stickyFooter?: boolean | undefined;
116
115
  object?: import("@evoke-platform/context").Obj | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-platform/ui-components",
3
- "version": "1.6.0-dev.18",
3
+ "version": "1.6.0-dev.19",
4
4
  "description": "",
5
5
  "main": "dist/published/index.js",
6
6
  "module": "dist/published/index.js",