@omnsight/osint-entity-components 0.2.4 → 0.2.6

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 (60) hide show
  1. package/dist/index.js +8 -8
  2. package/dist/index.mjs +796 -781
  3. package/package.json +15 -2
  4. package/src/App.tsx +397 -141
  5. package/src/avatars/layouts/AvatarDropdown.tsx +1 -1
  6. package/src/avatars/layouts/AvatarSpan.tsx +7 -2
  7. package/src/forms/BaseForm.tsx +138 -0
  8. package/src/forms/EditableAttributes.tsx +131 -0
  9. package/src/forms/EventForm/EditableForm.tsx +78 -0
  10. package/src/forms/EventForm/EditingForm.tsx +401 -0
  11. package/src/forms/EventForm/IconFormSection.tsx +54 -0
  12. package/src/forms/EventForm/StaticForm.tsx +272 -0
  13. package/src/forms/EventForm/index.ts +1 -0
  14. package/src/forms/InsightForm/EditableForm.tsx +70 -0
  15. package/src/forms/InsightForm/EditingForm.tsx +79 -0
  16. package/src/forms/InsightForm/StaticForm.tsx +139 -0
  17. package/src/forms/InsightForm/index.ts +1 -0
  18. package/src/forms/MonitoringSourceForm/EditableForm.tsx +59 -0
  19. package/src/forms/MonitoringSourceForm/EditingForm.tsx +192 -0
  20. package/src/forms/MonitoringSourceForm/StaticForm.tsx +107 -0
  21. package/src/forms/MonitoringSourceForm/index.ts +1 -0
  22. package/src/forms/OrganizationForm/EditableForm.tsx +74 -0
  23. package/src/forms/OrganizationForm/EditingForm.tsx +177 -0
  24. package/src/forms/OrganizationForm/IconFormSection.tsx +60 -0
  25. package/src/forms/OrganizationForm/StaticForm.tsx +209 -0
  26. package/src/forms/OrganizationForm/index.ts +1 -0
  27. package/src/forms/PersonForm/EditableForm.tsx +74 -0
  28. package/src/forms/PersonForm/EditingForm.tsx +187 -0
  29. package/src/forms/PersonForm/IconFormSection.tsx +54 -0
  30. package/src/forms/PersonForm/StaticForm.tsx +202 -0
  31. package/src/forms/PersonForm/index.ts +1 -0
  32. package/src/forms/RelationForm/EditableForm.tsx +74 -0
  33. package/src/forms/RelationForm/EditingForm.tsx +147 -0
  34. package/src/forms/RelationForm/StaticForm.tsx +182 -0
  35. package/src/forms/RelationForm/index.ts +1 -0
  36. package/src/forms/SourceForm/EditableForm.tsx +74 -0
  37. package/src/forms/SourceForm/EditingForm.tsx +199 -0
  38. package/src/forms/SourceForm/IconFormSection.tsx +54 -0
  39. package/src/forms/SourceForm/StaticForm.tsx +209 -0
  40. package/src/forms/SourceForm/index.ts +1 -0
  41. package/src/forms/WebsiteForm/EditableForm.tsx +74 -0
  42. package/src/forms/WebsiteForm/EditingForm.tsx +216 -0
  43. package/src/forms/WebsiteForm/IconFormSection.tsx +54 -0
  44. package/src/forms/WebsiteForm/StaticForm.tsx +225 -0
  45. package/src/forms/WebsiteForm/index.ts +1 -0
  46. package/src/forms/accessLevel.ts +48 -0
  47. package/src/forms/index.ts +8 -0
  48. package/src/icons/Event/Select.tsx +7 -6
  49. package/src/icons/Organization/Select.tsx +7 -6
  50. package/src/icons/Person/Select.tsx +7 -6
  51. package/src/icons/Source/Select.tsx +7 -6
  52. package/src/icons/Website/Select.tsx +9 -8
  53. package/src/inputs/CountrySelect.tsx +45 -0
  54. package/src/inputs/CustomDatePicker.tsx +51 -0
  55. package/src/inputs/CustomDateTimePicker.tsx +51 -0
  56. package/src/inputs/RangeDatePicker.tsx +99 -0
  57. package/src/inputs/TimezoneSelect.tsx +20 -0
  58. package/src/locales/en.json +135 -0
  59. package/src/locales/zh.json +135 -0
  60. package/src/main.tsx +20 -4
@@ -28,7 +28,7 @@ export const AvatarDropdown: React.FC<Props> = ({ children, avatarOnOpen, avatar
28
28
  radius="xl"
29
29
  onClick={() => setOpened((o) => !o)}
30
30
  >
31
- {opened ? avatarOnOpen : avatarOnClose}
31
+ {opened ? avatarOnClose : avatarOnOpen}
32
32
  </ActionIcon>
33
33
  </Popover.Target>
34
34
  <Popover.Dropdown
@@ -3,9 +3,14 @@ import { Avatar } from '@mantine/core';
3
3
  import { EmptyAvatar } from '../EmptyAvatar';
4
4
 
5
5
  interface Props {
6
+ showEmptyAvatar?: boolean;
6
7
  children: React.ReactNode[];
7
8
  }
8
9
 
9
- export const AvatarSpan: React.FC<Props> = ({ children }) => {
10
- return <Avatar.Group>{children.length === 0 ? <EmptyAvatar /> : children}</Avatar.Group>;
10
+ export const AvatarSpan: React.FC<Props> = ({ showEmptyAvatar = true, children }) => {
11
+ return (
12
+ <Avatar.Group>
13
+ {children.length === 0 && showEmptyAvatar ? <EmptyAvatar /> : children}
14
+ </Avatar.Group>
15
+ );
11
16
  };
@@ -0,0 +1,138 @@
1
+ import { modals } from '@mantine/modals';
2
+ import { Paper, Group, Title, ActionIcon, LoadingOverlay, Text, Tooltip } from '@mantine/core';
3
+ import { CheckCircleIcon, XMarkIcon } from '@heroicons/react/24/solid';
4
+ import {
5
+ useForm,
6
+ type DefaultValues,
7
+ type FieldValues,
8
+ type UseFormReturn,
9
+ type SubmitHandler,
10
+ FormProvider,
11
+ } from 'react-hook-form';
12
+ import { useTranslation } from 'react-i18next';
13
+ import { get } from 'lodash';
14
+ import { type CSSProperties } from 'react';
15
+
16
+ interface Props<T extends FieldValues> {
17
+ title: string;
18
+ icon?: React.ReactNode;
19
+ titleRight?: React.ReactNode;
20
+ onlyShowEditOnDirty: boolean;
21
+ onSubmit?: SubmitHandler<T>;
22
+ onUpdate?: (data: Partial<T>) => void;
23
+ onClose: () => void;
24
+ defaultValues: DefaultValues<T>;
25
+ exitButton?: React.ReactNode;
26
+ children: (methods: UseFormReturn<T>) => React.ReactNode;
27
+ style?: CSSProperties;
28
+ }
29
+
30
+ export function BaseForm<T extends FieldValues>({
31
+ title,
32
+ icon,
33
+ titleRight,
34
+ onlyShowEditOnDirty,
35
+ onSubmit,
36
+ onUpdate,
37
+ onClose,
38
+ defaultValues,
39
+ exitButton,
40
+ children,
41
+ style,
42
+ }: Props<T>) {
43
+ const { t } = useTranslation();
44
+ const methods = useForm<T>({ defaultValues });
45
+ const {
46
+ handleSubmit,
47
+ reset,
48
+ formState: { isSubmitting, isDirty, dirtyFields },
49
+ } = methods;
50
+
51
+ const truncateString = (str: string, num: number) => {
52
+ if (str.length <= num) return str;
53
+ return str.slice(0, num) + '...';
54
+ };
55
+
56
+ const handleInternalSubmit = handleSubmit(async (data) => {
57
+ if (onUpdate) {
58
+ const dirtyValues: Partial<T> = {};
59
+ for (const key in dirtyFields) {
60
+ if (dirtyFields[key]) {
61
+ dirtyValues[key as keyof T] = get(data, key);
62
+ }
63
+ }
64
+ onUpdate(dirtyValues);
65
+ reset(data);
66
+ } else if (onSubmit) {
67
+ onSubmit(data);
68
+ }
69
+ onClose();
70
+ });
71
+
72
+ const handleCancel = () => {
73
+ if (!isDirty) {
74
+ onClose();
75
+ } else {
76
+ modals.openConfirmModal({
77
+ title: t('components.forms.BaseForm.discardChanges'),
78
+ centered: true,
79
+ children: <Text size="sm">{t('components.forms.BaseForm.unsavedChangesWarning')}</Text>,
80
+ labels: { confirm: t('common.confirm'), cancel: t('common.cancel') },
81
+ confirmProps: { color: 'red' },
82
+ zIndex: 10001,
83
+ onConfirm: () => {
84
+ reset();
85
+ onClose();
86
+ },
87
+ });
88
+ }
89
+ };
90
+
91
+ return (
92
+ <Paper
93
+ p="md"
94
+ shadow="sm"
95
+ pos="relative"
96
+ style={{
97
+ display: 'flex',
98
+ flexDirection: 'column',
99
+ border: '1px solid var(--mantine-color-default-border)',
100
+ ...style,
101
+ }}
102
+ >
103
+ <LoadingOverlay visible={isSubmitting} overlayProps={{ radius: 'sm', blur: 1 }} />
104
+ <Group justify="space-between" mb="md">
105
+ <Group>
106
+ {icon}
107
+ <Tooltip label={title}>
108
+ <Title order={4}>{truncateString(title, 25)}</Title>
109
+ </Tooltip>
110
+ {titleRight}
111
+ {(!onlyShowEditOnDirty || isDirty) && (
112
+ <ActionIcon
113
+ radius="xl"
114
+ variant={isDirty ? 'filled' : 'subtle'}
115
+ color={isDirty ? 'green' : 'gray'}
116
+ onClick={handleInternalSubmit}
117
+ style={{
118
+ cursor: isDirty ? 'pointer' : 'not-allowed',
119
+ opacity: isDirty ? 1 : 0.5,
120
+ }}
121
+ disabled={!isDirty && !isSubmitting}
122
+ >
123
+ <CheckCircleIcon style={{ width: 16, height: 16 }} />
124
+ </ActionIcon>
125
+ )}
126
+ </Group>
127
+ {exitButton ? (exitButton) : (
128
+ <ActionIcon color="red" onClick={handleCancel}>
129
+ <XMarkIcon style={{ width: 16, height: 16 }} />
130
+ </ActionIcon>
131
+ )}
132
+ </Group>
133
+ <FormProvider {...methods}>
134
+ <form onSubmit={handleInternalSubmit}>{children(methods)}</form>
135
+ </FormProvider>
136
+ </Paper>
137
+ );
138
+ }
@@ -0,0 +1,131 @@
1
+ import { Button, Group, NumberInput, Select, Stack, Switch, Text, TextInput } from '@mantine/core';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { randomId } from '@mantine/hooks';
4
+
5
+ interface Props {
6
+ value: Record<string, any>;
7
+ onChange: (value: Record<string, any>) => void;
8
+ isEditing?: boolean;
9
+ }
10
+
11
+ export const EditableAttributes: React.FC<Props> = ({ value, onChange, isEditing }) => {
12
+ const { t } = useTranslation();
13
+
14
+ const handleAdd = () => {
15
+ onChange({ ...value, [randomId()]: { key: '', value: '', type: 'text' } });
16
+ };
17
+
18
+ const handleKeyChange = (id: string, newKey: string) => {
19
+ onChange({ ...value, [id]: { ...value[id], key: newKey } });
20
+ };
21
+
22
+ const handleValueChange = (id: string, newValue: any) => {
23
+ onChange({ ...value, [id]: { ...value[id], value: newValue } });
24
+ };
25
+
26
+ const handleTypeChange = (id: string, newType: 'text' | 'number' | 'toggle') => {
27
+ let defaultValue: any = '';
28
+ if (newType === 'number') {
29
+ defaultValue = 0;
30
+ } else if (newType === 'toggle') {
31
+ defaultValue = false;
32
+ }
33
+ onChange({ ...value, [id]: { ...(value[id] || {}), type: newType, value: defaultValue } });
34
+ };
35
+
36
+ const handleRemove = (id: string) => {
37
+ const { [id]: _, ...rest } = value;
38
+ onChange(rest);
39
+ };
40
+
41
+ if (!isEditing) {
42
+ return (
43
+ <Stack gap="xs">
44
+ {Object.entries(value)
45
+ .filter(
46
+ ([key, val]) =>
47
+ key !== 'icon_color' && val !== null && typeof val === 'object' && 'key' in val,
48
+ )
49
+ .map(([id, val]: [string, any]) => (
50
+ <Group key={id} gap="xs">
51
+ <Text size="sm" fw={500}>
52
+ {val.key}:
53
+ </Text>
54
+ <Text size="sm">{String(val.value)}</Text>
55
+ </Group>
56
+ ))}
57
+ </Stack>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <Stack gap="xs" pb="sm">
63
+ {Object.entries(value)
64
+ .filter(
65
+ ([key, val]) =>
66
+ key !== 'icon_color' && val !== null && typeof val === 'object' && 'key' in val,
67
+ )
68
+ .map(([id, val]: [string, any]) => {
69
+ return (
70
+ <Group key={id} gap="xs">
71
+ <TextInput
72
+ w={75}
73
+ value={val.key}
74
+ onChange={(e) => handleKeyChange(id, e.currentTarget.value)}
75
+ placeholder={t('placeholder.key')}
76
+ />
77
+ <Select
78
+ w={100}
79
+ value={val.type}
80
+ onChange={(newType) => {
81
+ if (newType) {
82
+ handleTypeChange(id, newType as 'text' | 'number' | 'toggle');
83
+ }
84
+ }}
85
+ placeholder={t('placeholder.type')}
86
+ data={[
87
+ { value: 'text', label: t('common.types.text') },
88
+ { value: 'number', label: t('common.types.number') },
89
+ { value: 'toggle', label: t('common.types.toggle') },
90
+ ]}
91
+ />
92
+ {val.type === 'text' && (
93
+ <TextInput
94
+ w={150}
95
+ value={val.value}
96
+ onChange={(e) => handleValueChange(id, e.currentTarget.value)}
97
+ placeholder={t('placeholder.value')}
98
+ />
99
+ )}
100
+ {val.type === 'number' && (
101
+ <NumberInput
102
+ w={150}
103
+ value={val.value}
104
+ onChange={(v) => handleValueChange(id, v)}
105
+ placeholder={t('placeholder.value')}
106
+ />
107
+ )}
108
+ {val.type === 'toggle' && (
109
+ <Switch
110
+ w={150}
111
+ checked={val.value}
112
+ onChange={(e) => handleValueChange(id, e.currentTarget.checked)}
113
+ />
114
+ )}
115
+ <Button color="red" variant="subtle" size="xs" onClick={() => handleRemove(id)}>
116
+ {t('common.remove')}
117
+ </Button>
118
+ </Group>
119
+ );
120
+ })}
121
+ <Button
122
+ onClick={handleAdd}
123
+ size="xs"
124
+ mt="sm"
125
+ disabled={Object.values(value).some((v: any) => v && v.key === '')}
126
+ >
127
+ {t('common.add')}
128
+ </Button>
129
+ </Stack>
130
+ );
131
+ };
@@ -0,0 +1,78 @@
1
+ import { useState, type PropsWithChildren, type CSSProperties } from "react";
2
+ import type { Event, Source, Permissive } from "omni-osint-crud-client";
3
+ import { EditingForm } from "./EditingForm";
4
+ import { StaticForm } from "./StaticForm";
5
+
6
+ interface Props extends PropsWithChildren {
7
+ event: Event;
8
+ sources?: Source[];
9
+ isAdmin?: boolean;
10
+ onSubmit?: (data: Event) => void;
11
+ onUpdate?: (data: Partial<Event>) => void;
12
+ onUpdatePermissive?: (data: Permissive) => void;
13
+ onClose?: () => void;
14
+ exitButton?: React.ReactNode;
15
+ style?: CSSProperties;
16
+ }
17
+
18
+ export const EventForm: React.FC<Props> = ({
19
+ event,
20
+ sources = [],
21
+ isAdmin = false,
22
+ onSubmit,
23
+ onUpdate,
24
+ onUpdatePermissive,
25
+ onClose,
26
+ exitButton,
27
+ children,
28
+ style,
29
+ }) => {
30
+ const [isEditing, setIsEditing] = useState(onSubmit !== undefined || false);
31
+
32
+ if (
33
+ onSubmit !== undefined &&
34
+ (onUpdate !== undefined || onUpdatePermissive !== undefined)
35
+ ) {
36
+ throw new Error(
37
+ "onSubmit cannot be defined at the same time with onUpdate or onUpdatePermissive",
38
+ );
39
+ }
40
+
41
+ const handlClose = () => {
42
+ if (onUpdate !== undefined) {
43
+ setIsEditing(false);
44
+ }
45
+ onClose?.();
46
+ };
47
+
48
+ const handleDoubleClick = () => {
49
+ if (onUpdate !== undefined) {
50
+ setIsEditing(true);
51
+ }
52
+ };
53
+
54
+ return isEditing ? (
55
+ <EditingForm
56
+ event={event}
57
+ sources={sources}
58
+ onSubmit={onSubmit}
59
+ onUpdate={onUpdate}
60
+ onClose={handlClose}
61
+ children={children}
62
+ style={style}
63
+ />
64
+ ) : (
65
+ <StaticForm
66
+ event={event}
67
+ sources={sources}
68
+ isAdmin={isAdmin}
69
+ onUpdate={onUpdatePermissive}
70
+ onClose={handlClose}
71
+ onDoubleClick={handleDoubleClick}
72
+ editModeEnabled={onUpdate !== undefined}
73
+ exitButton={exitButton || <></>}
74
+ children={children}
75
+ style={style}
76
+ />
77
+ );
78
+ };