@orchestrator-ui/orchestrator-ui-components 1.24.0 → 1.26.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.
Files changed (32) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +7 -7
  4. package/CHANGELOG.md +24 -0
  5. package/dist/index.d.ts +16 -13
  6. package/dist/index.js +1600 -1520
  7. package/package.json +1 -1
  8. package/src/components/WfoBadges/WfoWebsocketStatusBadge/WfoWebsocketStatusBadge.tsx +1 -1
  9. package/src/components/WfoForms/formFields/utils.spec.ts +24 -6
  10. package/src/components/WfoKeyValueTable/styles.ts +0 -1
  11. package/src/components/WfoSettingsModal/WfoSettingsModal.tsx +28 -22
  12. package/src/components/WfoSubscription/WfoInUseByRelations.tsx +1 -1
  13. package/src/components/WfoSubscription/WfoRelatedSubscriptions.tsx +2 -1
  14. package/src/components/WfoSubscription/WfoSubscriptionDetailTree.tsx +4 -24
  15. package/src/components/WfoSubscription/WfoSubscriptionProductBlock/WfoProductBlockKeyValueRow.tsx +2 -3
  16. package/src/components/WfoSubscription/WfoSubscriptionProductBlock/WfoSubscriptionProductBlock.tsx +93 -36
  17. package/src/components/WfoSubscription/WfoSubscriptionProductBlock/styles.ts +16 -10
  18. package/src/components/WfoTable/WfoTableSettingsModal/WfoTableSettingsModal.tsx +5 -2
  19. package/src/components/WfoTable/WfoTableWithFilter/WfoTableWithFilter.tsx +4 -4
  20. package/src/components/WfoTree/treeUtils.spec.ts +20 -4
  21. package/src/components/WfoWorkflowSteps/WfoStep/WfoStepForm.tsx +20 -6
  22. package/src/messages/en-GB.json +5 -1
  23. package/src/messages/nl-NL.json +5 -1
  24. package/src/pages/processes/WfoProcessDetailPage.tsx +23 -70
  25. package/src/pages/processes/WfoStartProcessPage.tsx +30 -3
  26. package/src/rtk/api.ts +2 -2
  27. package/src/rtk/endpoints/processDetail.ts +15 -4
  28. package/src/rtk/endpoints/processList.ts +1 -1
  29. package/src/rtk/endpoints/streamMessages.ts +25 -10
  30. package/src/rtk/endpoints/subscriptionDetail.ts +4 -1
  31. package/src/types/types.ts +1 -1
  32. package/src/utils/date.ts +3 -1
@@ -5,7 +5,6 @@ describe('getWfoTreeNodeDepth', () => {
5
5
  const field: FieldValue = { field: 'test', value: 'test' };
6
6
  const sampleTree: TreeBlock = {
7
7
  id: 1,
8
- ownerSubscriptionId: 'subscription-1',
9
8
  subscriptionInstanceId: 'subscription-1',
10
9
  parent: null,
11
10
  icon: 'folder',
@@ -14,10 +13,13 @@ describe('getWfoTreeNodeDepth', () => {
14
13
  productBlockInstanceValues: [field],
15
14
  inUseByRelations: [],
16
15
  isOutsideCurrentSubscription: false,
16
+ subscription: {
17
+ subscriptionId: 'subscription-1',
18
+ description: 'Subscription 1',
19
+ },
17
20
  children: [
18
21
  {
19
22
  id: 2,
20
- ownerSubscriptionId: 'subscription-2',
21
23
  subscriptionInstanceId: 'subscription-2',
22
24
  parent: 1,
23
25
  icon: 'file',
@@ -27,10 +29,13 @@ describe('getWfoTreeNodeDepth', () => {
27
29
  inUseByRelations: [],
28
30
  children: [],
29
31
  isOutsideCurrentSubscription: false,
32
+ subscription: {
33
+ subscriptionId: 'subscription-2',
34
+ description: 'Subscription 2',
35
+ },
30
36
  },
31
37
  {
32
38
  id: 3,
33
- ownerSubscriptionId: 'subscription-3',
34
39
  subscriptionInstanceId: 'subscription-3',
35
40
  parent: 1,
36
41
  icon: 'file',
@@ -39,10 +44,13 @@ describe('getWfoTreeNodeDepth', () => {
39
44
  productBlockInstanceValues: [field],
40
45
  inUseByRelations: [],
41
46
  isOutsideCurrentSubscription: false,
47
+ subscription: {
48
+ subscriptionId: 'subscription-3',
49
+ description: 'Subscription 3',
50
+ },
42
51
  children: [
43
52
  {
44
53
  id: 4,
45
- ownerSubscriptionId: 'subscription-4',
46
54
  subscriptionInstanceId: 'subscription-4',
47
55
  parent: 3,
48
56
  icon: 'file',
@@ -52,6 +60,10 @@ describe('getWfoTreeNodeDepth', () => {
52
60
  inUseByRelations: [],
53
61
  children: [],
54
62
  isOutsideCurrentSubscription: false,
63
+ subscription: {
64
+ subscriptionId: 'subscription-4',
65
+ description: 'Subscription 4',
66
+ },
55
67
  },
56
68
  ],
57
69
  },
@@ -96,6 +108,10 @@ describe('getWfoTreeNodeDepth', () => {
96
108
  inUseByRelations: [],
97
109
  children: [],
98
110
  isOutsideCurrentSubscription: false,
111
+ subscription: {
112
+ subscriptionId: 'subscription-1',
113
+ description: 'Subscription 1',
114
+ },
99
115
  };
100
116
 
101
117
  expect(() =>
@@ -2,8 +2,9 @@ import React, { useState } from 'react';
2
2
 
3
3
  import { EuiFlexItem } from '@elastic/eui';
4
4
 
5
- import { UserInputFormWizard, WfoLoading } from '@/components';
5
+ import { UserInputFormWizard, WfoError, WfoLoading } from '@/components';
6
6
  import { useOrchestratorTheme } from '@/hooks';
7
+ import { HttpStatus } from '@/rtk';
7
8
  import { useResumeProcessMutation } from '@/rtk/endpoints/forms';
8
9
  import { InputForm } from '@/types/forms';
9
10
 
@@ -19,6 +20,7 @@ export const WfoStepForm = ({
19
20
  processId,
20
21
  }: WfoStepFormProps) => {
21
22
  const [isProcessing, setIsProcessing] = useState<boolean>(false);
23
+ const [hasError, setHasError] = useState<boolean>(false);
22
24
  const { theme } = useOrchestratorTheme();
23
25
  const [resumeProcess] = useResumeProcessMutation();
24
26
 
@@ -27,16 +29,28 @@ export const WfoStepForm = ({
27
29
  return Promise.reject();
28
30
  }
29
31
 
30
- return resumeProcess({ processId, userInputs: processInput }).then(
31
- () => {
32
+ return resumeProcess({ processId, userInputs: processInput })
33
+ .unwrap()
34
+ .then(() => {
32
35
  setIsProcessing(true);
33
- },
34
- );
36
+ })
37
+ .catch((error) => {
38
+ if (error?.status !== HttpStatus.FormNotComplete) {
39
+ if (error?.status === HttpStatus.BadRequest) {
40
+ // Rethrow the error so userInputForm can catch it and display validation errors
41
+ throw error;
42
+ }
43
+ console.error(error);
44
+ setHasError(true);
45
+ } else {
46
+ throw error;
47
+ }
48
+ });
35
49
  };
36
50
 
37
51
  return (
38
52
  <EuiFlexItem css={{ margin: theme.size.m }}>
39
- {(isProcessing && <WfoLoading />) || (
53
+ {(hasError && <WfoError />) || (isProcessing && <WfoLoading />) || (
40
54
  <UserInputFormWizard
41
55
  stepUserInput={userInputForm}
42
56
  stepSubmit={submitForm}
@@ -17,7 +17,11 @@
17
17
  "darkMode": "Dark mode",
18
18
  "lightMode": "Light mode",
19
19
  "websocketConnected": "The websocket is connected",
20
- "websocketDisconnected": "The websocket is disconnected, click the icon or refresh the page"
20
+ "websocketDisconnected": "The websocket is disconnected, click the icon or refresh the page",
21
+ "resetToDefault": "Reset to default",
22
+ "savePreferences": "Save preferences",
23
+ "numberOfRows": "Number of rows",
24
+ "tableSettings": "Table settings"
21
25
  },
22
26
  "common": {
23
27
  "product": "Product",
@@ -17,7 +17,11 @@
17
17
  "darkMode": "Dark mode",
18
18
  "lightMode": "Light mode",
19
19
  "websocketConnected": "De websocket is actief",
20
- "websocketDisconnected": "De websocket verbinding is verbroken, klik op het icoon of ververs de pagina."
20
+ "websocketDisconnected": "De websocket verbinding is verbroken, klik op het icoon of ververs de pagina.",
21
+ "resetToDefault": "Reset naar standaard",
22
+ "savePreferences": "Voorkeuren opslaan",
23
+ "numberOfRows": "Aantal rijen",
24
+ "tableSettings": "Tabel instellingen"
21
25
  },
22
26
  "common": {
23
27
  "product": "Produkt",
@@ -1,19 +1,16 @@
1
- import React, { useEffect, useRef, useState } from 'react';
2
-
3
- import { TimelineItem, WfoError, WfoLoading } from '@/components';
4
- import { useGetProcessDetailQuery } from '@/rtk/endpoints/processDetail';
1
+ import React, { useRef } from 'react';
5
2
 
6
3
  import {
4
+ TimelineItem,
5
+ WfoError,
6
+ WfoLoading,
7
7
  WfoStepListRef,
8
8
  WfoWorkflowStepList,
9
- } from '../../components/WfoWorkflowSteps';
10
- import {
11
- ProcessDetail,
12
- ProcessDoneStatuses,
13
- ProcessStatus,
14
- Step,
15
- } from '../../types';
16
- import { getProductNamesFromProcess } from '../../utils';
9
+ } from '@/components';
10
+ import { useGetProcessDetailQuery } from '@/rtk/endpoints/processDetail';
11
+ import { Step } from '@/types';
12
+ import { getProductNamesFromProcess } from '@/utils';
13
+
17
14
  import { WfoProcessDetail } from './WfoProcessDetail';
18
15
  import {
19
16
  convertStepsToGroupedSteps,
@@ -24,69 +21,25 @@ export type GroupedStep = {
24
21
  steps: Step[];
25
22
  };
26
23
 
27
- const PROCESS_DETAIL_DEFAULT_REFETCH_INTERVAL = 3000;
28
-
29
24
  interface WfoProcessDetailPageProps {
30
25
  processId: string;
31
- processDetailRefetchInterval?: number;
32
26
  }
33
27
 
34
28
  export const WfoProcessDetailPage = ({
35
29
  processId,
36
- processDetailRefetchInterval = PROCESS_DETAIL_DEFAULT_REFETCH_INTERVAL,
37
30
  }: WfoProcessDetailPageProps) => {
38
31
  const stepListRef = useRef<WfoStepListRef>(null);
39
- const [fetchInterval, setFetchInterval] = useState<number | undefined>();
40
- const [process, setProcess] = useState<ProcessDetail | undefined>();
41
-
42
- const { data, isLoading, isError } = useGetProcessDetailQuery(
43
- { processId },
44
- { pollingInterval: fetchInterval },
45
- );
46
-
47
- if (isError) {
48
- if (fetchInterval) {
49
- setFetchInterval(undefined);
50
- }
51
- }
52
-
53
- useEffect(() => {
54
- const process = data?.processes[0];
55
- // We need to cast here because the backend might return the string in upperCase and
56
- // toLowerCase() will converts the value type to string
57
- const lastStatus =
58
- process?.lastStatus.toLocaleLowerCase() as ProcessStatus;
59
-
60
- const isInProgress = !(
61
- lastStatus && ProcessDoneStatuses.includes(lastStatus)
62
- );
63
-
64
- setFetchInterval(
65
- isInProgress ? processDetailRefetchInterval : undefined,
66
- );
67
- }, [data, processDetailRefetchInterval]);
68
-
69
- useEffect(() => {
70
- const fetchedProcessDetails = data?.processes[0];
71
-
72
- if (!process) {
73
- setProcess(fetchedProcessDetails);
74
- return;
75
- }
76
32
 
77
- const shouldUpdateProcess =
78
- process.lastStatus != fetchedProcessDetails?.lastStatus ||
79
- process.lastStep !== fetchedProcessDetails?.lastStep;
80
- if (shouldUpdateProcess) {
81
- setProcess(fetchedProcessDetails);
82
- }
83
- }, [data, process]);
33
+ const { data, isLoading, isError } = useGetProcessDetailQuery({
34
+ processId,
35
+ });
36
+ const processDetail = data?.processes[0];
84
37
 
85
- const steps = process?.steps ?? [];
38
+ const steps = processDetail?.steps ?? [];
86
39
 
87
- const productNames = getProductNamesFromProcess(process);
88
- const pageTitle = process?.workflowName || '';
89
- const isTask = process?.isTask ?? false;
40
+ const productNames = getProductNamesFromProcess(processDetail);
41
+ const pageTitle = processDetail?.workflowName || '';
42
+ const isTask = processDetail?.isTask ?? false;
90
43
  const groupedSteps: GroupedStep[] = convertStepsToGroupedSteps(steps);
91
44
  const timelineItems: TimelineItem[] =
92
45
  mapGroupedStepsToTimelineItems(groupedSteps);
@@ -96,7 +49,7 @@ export const WfoProcessDetailPage = ({
96
49
  pageTitle={pageTitle}
97
50
  productNames={productNames}
98
51
  buttonsAreDisabled={isLoading || isError}
99
- processDetail={process}
52
+ processDetail={processDetail}
100
53
  timelineItems={timelineItems}
101
54
  onTimelineItemClick={(id: string) =>
102
55
  stepListRef.current?.scrollToStep(id)
@@ -106,16 +59,16 @@ export const WfoProcessDetailPage = ({
106
59
  >
107
60
  {(isError && <WfoError />) ||
108
61
  (isLoading && <WfoLoading />) ||
109
- (process !== undefined && (
62
+ (processDetail !== undefined && (
110
63
  <WfoWorkflowStepList
111
64
  ref={stepListRef}
112
- processId={process.processId}
65
+ processId={processDetail.processId}
113
66
  steps={groupedSteps.flatMap(
114
67
  (groupedStep) => groupedStep.steps,
115
68
  )}
116
- traceBack={process.traceback}
117
- userInputForm={process.form}
118
- startedAt={process.startedAt}
69
+ traceBack={processDetail.traceback}
70
+ userInputForm={processDetail.form}
71
+ startedAt={processDetail.startedAt}
119
72
  isTask={isTask}
120
73
  />
121
74
  )) || <h1>Invalid processId</h1>}
@@ -22,6 +22,7 @@ import {
22
22
  handlePromiseErrorWithCallback,
23
23
  useGetTimeLineItemsQuery,
24
24
  } from '@/rtk';
25
+ import { useGetSubscriptionDetailQuery } from '@/rtk';
25
26
  import { useStartProcessMutation } from '@/rtk/endpoints/forms';
26
27
  import {
27
28
  EngineStatus,
@@ -87,6 +88,17 @@ export const WfoStartProcessPage = ({
87
88
  const [form, setForm] = useState<UserInputForm>({});
88
89
  const { productId, subscriptionId } = router.query as StartProcessPageQuery;
89
90
 
91
+ const {
92
+ data: subscriptionDetail,
93
+ isLoading: isLoadingSubscriptionDetail,
94
+ isError: isErrorSubscriptionDetail,
95
+ } = useGetSubscriptionDetailQuery(
96
+ {
97
+ subscriptionId: subscriptionId || '',
98
+ },
99
+ { skip: !subscriptionId },
100
+ );
101
+
90
102
  const [startProcess] = useStartProcessMutation();
91
103
 
92
104
  const startProcessPayload = useMemo(
@@ -101,10 +113,13 @@ export const WfoStartProcessPage = ({
101
113
 
102
114
  const {
103
115
  data: timeLineItems = [],
104
- isError,
105
- isLoading,
116
+ isError: isErrorTimeLineItems,
117
+ isLoading: isLoadingTimeLineItems,
106
118
  } = useGetTimeLineItemsQuery(processName);
107
119
 
120
+ const isLoading = isLoadingSubscriptionDetail || isLoadingTimeLineItems;
121
+ const isError = isErrorSubscriptionDetail || isErrorTimeLineItems;
122
+
108
123
  if (isError) {
109
124
  if (!hasError) {
110
125
  setHasError(true);
@@ -126,7 +141,7 @@ export const WfoStartProcessPage = ({
126
141
  const basePath = isTask
127
142
  ? PATH_TASKS
128
143
  : PATH_WORKFLOWS;
129
- router.push(`${basePath}/${process.id}`);
144
+ router.replace(`${basePath}/${process.id}`);
130
145
  }
131
146
  },
132
147
  // Reject handler
@@ -186,6 +201,18 @@ export const WfoStartProcessPage = ({
186
201
  lastStep: StepStatus.FORM,
187
202
  workflowName: processName,
188
203
  createdBy: '-',
204
+ subscriptions: subscriptionDetail && {
205
+ page: [
206
+ {
207
+ product: {
208
+ name: subscriptionDetail.subscription.product.name,
209
+ },
210
+ description: subscriptionDetail.subscription.description,
211
+ subscriptionId:
212
+ subscriptionDetail.subscription.subscriptionId,
213
+ },
214
+ ],
215
+ },
189
216
  };
190
217
 
191
218
  return (
package/src/rtk/api.ts CHANGED
@@ -16,7 +16,7 @@ export enum BaseQueryTypes {
16
16
  export enum CacheTags {
17
17
  engineStatus = 'engineStatus',
18
18
  cacheNames = 'cacheNames',
19
- processList = 'processList',
19
+ processes = 'processes',
20
20
  processListSummary = 'processListSummary',
21
21
  subscription = 'subscription',
22
22
  subscriptionList = 'subscriptionList',
@@ -108,7 +108,7 @@ export const orchestratorApi = createApi({
108
108
  tagTypes: [
109
109
  CacheTags.engineStatus,
110
110
  CacheTags.cacheNames,
111
- CacheTags.processList,
111
+ CacheTags.processes,
112
112
  CacheTags.processListSummary,
113
113
  CacheTags.subscriptionList,
114
114
  CacheTags.subscription,
@@ -86,6 +86,17 @@ const processDetailApi = orchestratorApi.injectEndpoints({
86
86
  processes,
87
87
  };
88
88
  },
89
+ providesTags: (result, error, queryArguments) => {
90
+ if (!error && result) {
91
+ return [
92
+ {
93
+ type: CacheTags.processes,
94
+ id: queryArguments.processId,
95
+ },
96
+ ];
97
+ }
98
+ return [];
99
+ },
89
100
  }),
90
101
  getRawProcessDetail: builder.query<
91
102
  ProcessDetailResultRaw,
@@ -111,7 +122,7 @@ const processDetailApi = orchestratorApi.injectEndpoints({
111
122
  extraOptions: {
112
123
  baseQueryType: BaseQueryTypes.fetch,
113
124
  },
114
- invalidatesTags: [CacheTags.processList],
125
+ invalidatesTags: [CacheTags.processes],
115
126
  }),
116
127
  retryProcess: builder.mutation<void, { processId: string }>({
117
128
  query: ({ processId }) => ({
@@ -126,7 +137,7 @@ const processDetailApi = orchestratorApi.injectEndpoints({
126
137
  extraOptions: {
127
138
  baseQueryType: BaseQueryTypes.fetch,
128
139
  },
129
- invalidatesTags: [CacheTags.processList],
140
+ invalidatesTags: [CacheTags.processes],
130
141
  }),
131
142
  deleteProcess: builder.mutation<void, { processId: string }>({
132
143
  query: ({ processId }) => ({
@@ -137,7 +148,7 @@ const processDetailApi = orchestratorApi.injectEndpoints({
137
148
  extraOptions: {
138
149
  baseQueryType: BaseQueryTypes.fetch,
139
150
  },
140
- invalidatesTags: [CacheTags.processList],
151
+ invalidatesTags: [CacheTags.processes],
141
152
  }),
142
153
  abortProcess: builder.mutation<void, { processId: string }>({
143
154
  query: ({ processId }) => ({
@@ -148,7 +159,7 @@ const processDetailApi = orchestratorApi.injectEndpoints({
148
159
  extraOptions: {
149
160
  baseQueryType: BaseQueryTypes.fetch,
150
161
  },
151
- invalidatesTags: [CacheTags.processList],
162
+ invalidatesTags: [CacheTags.processes],
152
163
  }),
153
164
  }),
154
165
  });
@@ -82,7 +82,7 @@ const processApi = orchestratorApi.injectEndpoints({
82
82
  pageInfo: response.processes?.pageInfo || {},
83
83
  };
84
84
  },
85
- providesTags: [CacheTags.processList],
85
+ providesTags: [CacheTags.processes],
86
86
  }),
87
87
  }),
88
88
  });
@@ -27,6 +27,8 @@ type WebSocketMessage = {
27
27
  value: string[] | string;
28
28
  };
29
29
 
30
+ type CacheInvalidationTag = CacheTags | { type: CacheTags; id: string };
31
+
30
32
  enum MessageTypes {
31
33
  invalidateCache = 'invalidateCache',
32
34
  }
@@ -66,17 +68,25 @@ const streamMessagesApi = orchestratorApi.injectEndpoints({
66
68
  updateCachedData(() => false);
67
69
  };
68
70
 
69
- const invalidateTag = (tag: string) => {
70
- const tagToInvalidate = tag as CacheTags;
71
- if (validCacheTags.includes(tagToInvalidate)) {
71
+ const invalidateTag = (cacheTag: CacheTags, id?: string) => {
72
+ if (validCacheTags.includes(cacheTag)) {
73
+ // If we receive an object with cacheTag and id we both invalidate the cache key and the cache key with the id
74
+ // invalidating both the general and specific caches
75
+ const cacheTags: CacheInvalidationTag[] = [cacheTag];
76
+
77
+ if (id) {
78
+ cacheTags.push({
79
+ type: cacheTag,
80
+ id,
81
+ });
82
+ }
83
+
72
84
  const cacheInvalidationAction =
73
- orchestratorApi.util.invalidateTags([
74
- tagToInvalidate,
75
- ]);
85
+ orchestratorApi.util.invalidateTags(cacheTags);
76
86
  dispatch(cacheInvalidationAction);
77
87
  } else {
78
88
  console.error(
79
- `Trying to invalidate a cache entry with an unknown tag: ${tagToInvalidate}`,
89
+ `Trying to invalidate a cache entry with an unknown tag: ${cacheTag}`,
80
90
  );
81
91
  }
82
92
  };
@@ -88,9 +98,14 @@ const streamMessagesApi = orchestratorApi.injectEndpoints({
88
98
  const messageValue = message.value;
89
99
 
90
100
  if (typeof messageValue === 'string') {
91
- invalidateTag(messageValue);
92
- } else if (Array.isArray(messageValue)) {
93
- messageValue.forEach((tag) => invalidateTag(tag));
101
+ invalidateTag(messageValue as CacheTags);
102
+ } else if (
103
+ Array.isArray(messageValue) &&
104
+ messageValue.length === 2
105
+ ) {
106
+ const [tag, id] = messageValue;
107
+ const cacheTag = tag as CacheTags;
108
+ invalidateTag(cacheTag, id);
94
109
  } else {
95
110
  console.error(
96
111
  'invalid message value type',
@@ -43,7 +43,10 @@ export const subscriptionDetailQuery = `
43
43
  }
44
44
  productBlockInstances {
45
45
  id
46
- ownerSubscriptionId
46
+ subscription {
47
+ subscriptionId
48
+ description
49
+ }
47
50
  parent
48
51
  productBlockInstanceValues
49
52
  subscriptionInstanceId
@@ -39,11 +39,11 @@ export type InUseByRelation = {
39
39
 
40
40
  export type ProductBlockInstance = {
41
41
  id: number;
42
- ownerSubscriptionId: string;
43
42
  subscriptionInstanceId: string;
44
43
  parent: Nullable<number>;
45
44
  productBlockInstanceValues: FieldValue[];
46
45
  inUseByRelations: InUseByRelation[];
46
+ subscription: Pick<Subscription, 'subscriptionId' | 'description'>;
47
47
  };
48
48
 
49
49
  export interface ResourceTypeDefinition {
package/src/utils/date.ts CHANGED
@@ -2,7 +2,9 @@ import { Locale } from '../types/types';
2
2
 
3
3
  export const getCurrentBrowserLocale = () => window.navigator.language;
4
4
 
5
- export const parseDate = (date: string | null | undefined): Date | null => {
5
+ export const parseDate = (
6
+ date: string | null | undefined | number,
7
+ ): Date | null => {
6
8
  if (date === null || date === undefined || date === '') {
7
9
  return null;
8
10
  }