Skip to content

Commit 9b071da

Browse files
--wip-- [skip ci]
1 parent 7f25b6d commit 9b071da

File tree

16 files changed

+723
-5
lines changed

16 files changed

+723
-5
lines changed

datahub-web-react/src/alchemy-components/components/Editor/extensions/fileDragDrop/fileUtils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,27 @@ export const isFileUrl = (url: string): boolean => {
245245
return url.includes('/openapi/v1/'); // Our internal file API
246246
};
247247

248+
export const extractFileNameFromUrl = (url: string): string | undefined => {
249+
if (!isFileUrl(url)) return undefined;
250+
251+
try {
252+
const urlObj = new URL(url);
253+
const pathname = urlObj.pathname;
254+
255+
// Extract the last part after the final '/'
256+
const lastSegment = pathname.split('/').pop();
257+
258+
if (!lastSegment) return undefined;
259+
260+
const fileName = lastSegment.split('__')?.[1];
261+
262+
return fileName;
263+
} catch (error) {
264+
// If URL parsing fails, return undefined
265+
return undefined;
266+
}
267+
};
268+
248269
/**
249270
* Get icon to show based on file extension
250271
* @param extension - the extension of the file

datahub-web-react/src/app/entityV2/shared/components/styled/AddLinkModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import React, { useState } from 'react';
55
import analytics, { EntityActionType, EventType } from '@app/analytics';
66
import { useUserContext } from '@app/context/useUserContext';
77
import { useEntityData, useMutationUrn } from '@app/entity/shared/EntityContext';
8-
import { FormData, LinkFormModal } from '@app/entityV2/shared/components/styled/LinkFormModal';
8+
import { LinkFormModal } from '@app/entityV2/shared/components/styled/LinkFormModal/LinkFormModal';
99
import { Button } from '@src/alchemy-components';
1010

1111
import { useAddLinkMutation } from '@graphql/mutations.generated';
12+
import { LinkFormData } from './LinkFormModal/types';
1213

1314
interface Props {
1415
buttonProps?: Record<string, unknown>;
@@ -31,7 +32,7 @@ export const AddLinkModal = ({ buttonProps, refetch, buttonType }: Props) => {
3132
setIsModalVisible(false);
3233
};
3334

34-
const handleAdd = async (formData: FormData) => {
35+
const handleAdd = async (formData: LinkFormData) => {
3536
if (user?.urn) {
3637
try {
3738
await addLinkMutation({

datahub-web-react/src/app/entityV2/shared/components/styled/LinkFormModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export const LinkFormModal = ({ open, initialValues, variant, onSubmit, onCancel
8989
color="gray"
9090
onClick={() => setIsShowInAssetPreview(!shouldBeShownInAssetPreview)}
9191
>
92-
Add to asset header
92+
Add to asset header1
9393
</FooterCheckboxLabel>
9494
</FooterCheckboxContainer>
9595
<FooterButtonsContainer>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Button, Icon, Text, colors } from '@components';
2+
import React, { useCallback, useRef, useState } from 'react';
3+
import styled from 'styled-components';
4+
5+
const Container = styled.div<{ $dragActive?: boolean }>`
6+
padding: 16px;
7+
8+
display: flex;
9+
flex-direction: column;
10+
align-items: center;
11+
12+
border: 1px dashed ${(props) => (props.$dragActive ? colors.primary[500] : colors.gray[100])};
13+
border-radius: 12px;
14+
`;
15+
16+
const InnerContainer = styled.div`
17+
display: flex;
18+
flex-direction: column;
19+
align-items: center;
20+
21+
gap: 8px;
22+
`;
23+
24+
const IconContainer = styled.div`
25+
display: flex;
26+
align-items: center;
27+
justify-content: center;
28+
29+
width: 32px;
30+
height: 32px;
31+
border-radius: 100%;
32+
background-color: ${colors.gray[1000]};
33+
`;
34+
35+
const ActionTextContainer = styled.div`
36+
display: flex;
37+
gap: 4px;
38+
`;
39+
40+
const InlineButton = styled(Button)`
41+
display: inline;
42+
padding: 0px;
43+
background: none;
44+
45+
&:hover {
46+
background: none;
47+
}
48+
`;
49+
50+
const Description = styled.div``;
51+
52+
interface Props {
53+
onFilesUpload?: (files: File[]) => Promise<void>;
54+
}
55+
56+
export function FileDragAndDropArea({ onFilesUpload }: Props) {
57+
const [dragActive, setDragActive] = useState<boolean>(false);
58+
59+
const inputRef = useRef<HTMLInputElement>(null);
60+
61+
const handleDrop = useCallback(
62+
async (e: React.DragEvent) => {
63+
e.preventDefault();
64+
e.stopPropagation();
65+
setDragActive(false);
66+
67+
await onFilesUpload?.(Array.from(e.dataTransfer.files));
68+
},
69+
[onFilesUpload],
70+
);
71+
72+
const onFileInputChange = useCallback(
73+
async (e: React.ChangeEvent<HTMLInputElement>) => {
74+
const files = e.target.files;
75+
if (files) await onFilesUpload?.(Array.from(files));
76+
},
77+
[onFilesUpload],
78+
);
79+
80+
const onButtonClick = (e: React.MouseEvent) => {
81+
e.stopPropagation();
82+
e.preventDefault();
83+
inputRef.current?.click();
84+
};
85+
86+
const handleDrag = useCallback((e: React.DragEvent) => {
87+
e.preventDefault();
88+
e.stopPropagation();
89+
if (e.type === 'dragenter' || e.type === 'dragover') {
90+
setDragActive(true);
91+
} else if (e.type === 'dragleave') {
92+
// Check if the next target is still inside our drop zone
93+
const dropZone = e.currentTarget;
94+
const related = e.relatedTarget;
95+
96+
// If relatedTarget is null (e.g., leaving window) or outside drop zone
97+
if (!related || !dropZone.contains(related as Node)) {
98+
setDragActive(false);
99+
}
100+
}
101+
}, []);
102+
103+
return (
104+
<>
105+
<Container
106+
onDrop={handleDrop}
107+
onDragEnter={handleDrag}
108+
onDragOver={handleDrag}
109+
onDragLeave={handleDrag}
110+
$dragActive={dragActive}
111+
>
112+
<InnerContainer>
113+
<IconContainer onDragLeave={(e) => e.stopPropagation()}>
114+
<Icon icon="UploadSimple" source="phosphor" color="primary" size="2xl" />
115+
</IconContainer>
116+
<ActionTextContainer>
117+
<Text size="sm" weight="semiBold">
118+
Drag a file or
119+
</Text>{' '}
120+
<InlineButton variant="text" size="sm" onClick={onButtonClick}>
121+
click to upload
122+
</InlineButton>
123+
</ActionTextContainer>
124+
<Description>
125+
<Text size="sm" color="gray">
126+
Max Size: 2GB
127+
</Text>
128+
</Description>
129+
</InnerContainer>
130+
</Container>
131+
132+
<input ref={inputRef} type="file" multiple onChange={onFileInputChange} style={{ display: 'none' }} />
133+
</>
134+
);
135+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Button, Icon } from '@components';
2+
import React, { useCallback } from 'react';
3+
import styled from 'styled-components';
4+
5+
import {
6+
extractFileNameFromUrl,
7+
getExtensionFromFileName,
8+
getFileIconFromExtension,
9+
} from '@components/components/Editor/extensions/fileDragDrop/fileUtils';
10+
11+
const Container = styled.div`
12+
display: flex;
13+
gap: 4px;
14+
align-items: center;
15+
`;
16+
17+
interface Props {
18+
url: string;
19+
onClose?: () => void;
20+
}
21+
22+
export function FileItem({ url, onClose }: Props) {
23+
const name = extractFileNameFromUrl(url);
24+
const extension = getExtensionFromFileName(name || '');
25+
const icon = getFileIconFromExtension(extension || '');
26+
27+
const closeHandler = useCallback(
28+
(e: React.MouseEvent) => {
29+
e.preventDefault();
30+
e.stopPropagation();
31+
onClose?.();
32+
},
33+
[onClose],
34+
);
35+
36+
return (
37+
<Container>
38+
<Icon icon={icon} size="lg" source="phosphor" color="primary" />
39+
{name}
40+
{onClose && (
41+
<Button
42+
icon={{
43+
icon: 'X',
44+
source: 'phosphor',
45+
color: 'gray',
46+
}}
47+
variant="link"
48+
onClick={closeHandler}
49+
/>
50+
)}
51+
</Container>
52+
);
53+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Form } from 'antd';
2+
import { FormInstance } from 'antd/es/form/Form';
3+
import { useCallback } from 'react';
4+
5+
import { useAppConfig } from '@app/useAppConfig';
6+
7+
import { UploadFileOrUrlLinkForm } from './UploadFileOrUrlLinkForm';
8+
import { UrlLinkForm } from './UrlLinkForm';
9+
import { LinkFormData } from './types';
10+
11+
interface Props {
12+
form: FormInstance<LinkFormData>;
13+
initialValues?: Partial<LinkFormData>;
14+
onSubmit: (formData: LinkFormData) => void;
15+
}
16+
17+
export function LinkForm({ form, initialValues, onSubmit }: Props) {
18+
const {
19+
config: {
20+
featureFlags: { documentationFileUploadV1 },
21+
},
22+
} = useAppConfig();
23+
24+
const renderForm = useCallback(() => {
25+
if (!documentationFileUploadV1) {
26+
return <UploadFileOrUrlLinkForm initialValues={initialValues} />;
27+
} else {
28+
return <UrlLinkForm initialValues={initialValues} />;
29+
}
30+
}, [initialValues, documentationFileUploadV1]);
31+
32+
return (
33+
<Form form={form} name="linkForm" onFinish={onSubmit} layout="vertical">
34+
{renderForm()}
35+
36+
<Form.Item
37+
data-testid="link-modal-show-in-asset-preview"
38+
name="showInAssetPreview"
39+
valuePropName="checked"
40+
initialValue={initialValues?.showInAssetPreview}
41+
hidden
42+
>
43+
<input type="text" />
44+
</Form.Item>
45+
46+
<Form.Item
47+
data-testid="link-modal-show-in-asset-preview"
48+
name="showInAssetPreview"
49+
valuePropName="checked"
50+
initialValue={initialValues?.showInAssetPreview}
51+
hidden
52+
>
53+
<input type="checkbox" />
54+
</Form.Item>
55+
</Form>
56+
);
57+
}

0 commit comments

Comments
 (0)