@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
@@ -0,0 +1,401 @@
1
+ import {
2
+ useMemo,
3
+ useState,
4
+ type PropsWithChildren,
5
+ type CSSProperties,
6
+ } from "react";
7
+ import {
8
+ CalendarDaysIcon,
9
+ ChevronDownIcon,
10
+ MapPinIcon,
11
+ } from "@heroicons/react/24/outline";
12
+ import {
13
+ Collapse,
14
+ Divider,
15
+ Group,
16
+ Stack,
17
+ Title,
18
+ UnstyledButton,
19
+ rem,
20
+ Text,
21
+ TextInput,
22
+ Textarea,
23
+ Select,
24
+ TagsInput,
25
+ NumberInput,
26
+ } from "@mantine/core";
27
+ import { CustomDateTimePicker } from "../../inputs/CustomDateTimePicker";
28
+ import { type Event, type Source } from "omni-osint-crud-client";
29
+ import { useTranslation } from "react-i18next";
30
+ import { Controller } from "react-hook-form";
31
+ import { EditableAttributes } from "../EditableAttributes";
32
+ import { BaseForm } from "../BaseForm";
33
+ import { EventIcon } from "@omnsight/osint-entity-components/icons";
34
+ import {
35
+ SourceLink,
36
+ AvatarSpan,
37
+ } from "@omnsight/osint-entity-components/avatars";
38
+ import { IconFormSection } from "./IconFormSection";
39
+ import countries from "i18n-iso-countries";
40
+ import enLocale from 'i18n-iso-countries/langs/en.json';
41
+ import zhLocale from 'i18n-iso-countries/langs/zh.json';
42
+
43
+ // Register locales
44
+ countries.registerLocale(enLocale);
45
+ countries.registerLocale(zhLocale);
46
+
47
+ interface Props extends PropsWithChildren {
48
+ event: Event;
49
+ sources?: Source[];
50
+ onSubmit?: (data: Event) => void;
51
+ onUpdate?: (data: Partial<Event>) => void;
52
+ onClose?: () => void;
53
+ style?: CSSProperties;
54
+ }
55
+
56
+ export const EditingForm: React.FC<Props> = ({
57
+ event,
58
+ sources = [],
59
+ onSubmit,
60
+ onUpdate,
61
+ onClose,
62
+ children,
63
+ style,
64
+ }) => {
65
+ const { t, i18n } = useTranslation();
66
+ const [attributesOpen, setAttributesOpen] = useState(false);
67
+
68
+ // Generate country list based on current language
69
+ const countryOptions = useMemo(() => {
70
+ // Map i18next language code to i18n-iso-countries language code
71
+ // 'zh' in i18next -> 'zh' in i18n-iso-countries
72
+ const lang = i18n.language.startsWith("zh") ? "zh" : "en";
73
+ const countryObj = countries.getNames(lang, { select: "official" });
74
+
75
+ return Object.entries(countryObj)
76
+ .map(([code, name]) => ({
77
+ value: code,
78
+ label: name,
79
+ }))
80
+ .sort((a, b) => a.label.localeCompare(b.label));
81
+ }, [i18n.language]);
82
+
83
+ const handlClose = () => {
84
+ onClose?.();
85
+ };
86
+
87
+ return (
88
+ <BaseForm<Event>
89
+ style={style}
90
+ icon={<EventIcon event={event} />}
91
+ title={t("components.forms.EventForm.title")}
92
+ titleRight={
93
+ sources && (
94
+ <AvatarSpan showEmptyAvatar={false}>
95
+ {sources.map((source) => (
96
+ <SourceLink key={source._id || source._key} data={source} />
97
+ ))}
98
+ </AvatarSpan>
99
+ )
100
+ }
101
+ onlyShowEditOnDirty={false}
102
+ onClose={handlClose}
103
+ defaultValues={{
104
+ ...event,
105
+ location: {
106
+ ...event.location,
107
+ sub_locality: event?.location?.sub_locality || "",
108
+ sub_administrative_area:
109
+ event?.location?.sub_administrative_area || "",
110
+ },
111
+ }}
112
+ onSubmit={onSubmit}
113
+ onUpdate={onUpdate}
114
+ >
115
+ {({ control, formState: { errors } }) => {
116
+ return (
117
+ <Stack
118
+ pos="relative"
119
+ gap="xs"
120
+ style={{
121
+ cursor: "default",
122
+ }}
123
+ >
124
+ <Text size="sm" fw={500}>
125
+ {t("components.forms.EventForm.title")}
126
+ </Text>
127
+ <Controller
128
+ name="title"
129
+ control={control}
130
+ rules={{ required: t("common.required") }}
131
+ render={({ field }) => (
132
+ <TextInput
133
+ {...field}
134
+ value={field.value || ""}
135
+ placeholder={t("components.forms.EventForm.title")}
136
+ error={errors.title?.message}
137
+ />
138
+ )}
139
+ />
140
+
141
+ <Group gap={4}>
142
+ <Text size="sm" c="dimmed">
143
+ {t("placeholder.type")}:
144
+ </Text>
145
+ <IconFormSection data={event} />
146
+ </Group>
147
+
148
+ <Text size="sm" fw={500}>
149
+ {t("placeholder.date")}
150
+ </Text>
151
+ <Group gap="xs" c="dimmed">
152
+ <CalendarDaysIcon style={{ width: rem(18), height: rem(18) }} />
153
+ <Controller
154
+ name="happened_at"
155
+ control={control}
156
+ rules={{ required: t("common.required") }}
157
+ render={({ field }) => (
158
+ <CustomDateTimePicker
159
+ value={field.value ? new Date(field.value * 1000) : null}
160
+ onChange={(date: Date | null) => {
161
+ if (date) {
162
+ field.onChange(date.getTime() / 1000);
163
+ } else {
164
+ field.onChange(undefined);
165
+ }
166
+ }}
167
+ placeholder={t("placeholder.date")}
168
+ error={errors.happened_at?.message}
169
+ />
170
+ )}
171
+ />
172
+ </Group>
173
+
174
+ <Text size="sm" fw={500}>
175
+ {t("placeholder.location")}
176
+ </Text>
177
+ <Group gap="xs" c="dimmed" align="flex-start" wrap="nowrap">
178
+ <MapPinIcon
179
+ style={{
180
+ width: rem(18),
181
+ height: rem(18),
182
+ flexShrink: 0,
183
+ marginTop: rem(2),
184
+ }}
185
+ />
186
+ <Stack gap={0} style={{ flex: 1 }}>
187
+ <Controller
188
+ name="location.address"
189
+ control={control}
190
+ rules={{ required: t("common.required") }}
191
+ render={({ field }) => (
192
+ <TextInput
193
+ {...field}
194
+ value={field.value || ""}
195
+ placeholder={t("placeholder.address")}
196
+ error={errors.location?.address?.message}
197
+ />
198
+ )}
199
+ />
200
+ <Group grow>
201
+ <Controller
202
+ name="location.locality"
203
+ control={control}
204
+ rules={{ required: t("common.required") }}
205
+ render={({ field }) => (
206
+ <TextInput
207
+ {...field}
208
+ value={field.value || ""}
209
+ placeholder={t("placeholder.locality")}
210
+ error={errors.location?.locality?.message}
211
+ />
212
+ )}
213
+ />
214
+ <Controller
215
+ name="location.sub_locality"
216
+ control={control}
217
+ render={({ field }) => (
218
+ <TextInput
219
+ {...field}
220
+ value={field.value || ""}
221
+ placeholder={t("placeholder.subLocality")}
222
+ />
223
+ )}
224
+ />
225
+ </Group>
226
+ <Group grow>
227
+ <Controller
228
+ name="location.administrative_area"
229
+ control={control}
230
+ rules={{ required: t("common.required") }}
231
+ render={({ field }) => (
232
+ <TextInput
233
+ {...field}
234
+ value={field.value || ""}
235
+ placeholder={t("placeholder.adminArea")}
236
+ error={errors.location?.administrative_area?.message}
237
+ />
238
+ )}
239
+ />
240
+ <Controller
241
+ name="location.sub_administrative_area"
242
+ control={control}
243
+ render={({ field }) => (
244
+ <TextInput
245
+ {...field}
246
+ value={field.value || ""}
247
+ placeholder={t("placeholder.subAdminArea")}
248
+ />
249
+ )}
250
+ />
251
+ </Group>
252
+ <Group grow>
253
+ <Controller
254
+ name="location.latitude"
255
+ control={control}
256
+ rules={{
257
+ required: t("common.required"),
258
+ min: {
259
+ value: -90,
260
+ message: t("components.forms.EventForm.minLatitude"),
261
+ },
262
+ max: {
263
+ value: 90,
264
+ message: t("components.forms.EventForm.maxLatitude"),
265
+ },
266
+ }}
267
+ render={({ field }) => (
268
+ <NumberInput
269
+ {...field}
270
+ value={field.value}
271
+ placeholder={t("placeholder.latitude")}
272
+ error={errors.location?.latitude?.message}
273
+ />
274
+ )}
275
+ />
276
+ <Controller
277
+ name="location.longitude"
278
+ control={control}
279
+ rules={{
280
+ required: t("common.required"),
281
+ min: {
282
+ value: -180,
283
+ message: t(
284
+ "components.forms.EventForm.minLongitude",
285
+ ),
286
+ },
287
+ max: {
288
+ value: 180,
289
+ message: t("components.forms.EventForm.maxLongitude"),
290
+ },
291
+ }}
292
+ render={({ field }) => (
293
+ <NumberInput
294
+ {...field}
295
+ value={field.value}
296
+ placeholder={t("placeholder.longitude")}
297
+ error={errors.location?.longitude?.message}
298
+ />
299
+ )}
300
+ />
301
+ </Group>
302
+ <Controller
303
+ name="location.country_code"
304
+ control={control}
305
+ rules={{ required: t("common.required") }}
306
+ render={({ field }) => (
307
+ <Select
308
+ {...field}
309
+ value={field.value || ""}
310
+ placeholder={t("placeholder.country")}
311
+ data={countryOptions}
312
+ searchable
313
+ clearable
314
+ error={errors.location?.country_code?.message}
315
+ />
316
+ )}
317
+ />
318
+ <Controller
319
+ name="location.postal_code"
320
+ control={control}
321
+ rules={{ required: t("common.required") }}
322
+ render={({ field }) => (
323
+ <TextInput
324
+ {...field}
325
+ value={field.value || ""}
326
+ placeholder={t("placeholder.postalCode")}
327
+ error={errors.location?.postal_code?.message}
328
+ />
329
+ )}
330
+ />
331
+ </Stack>
332
+ </Group>
333
+
334
+ <Text size="sm" fw={500}>
335
+ {t("placeholder.description")}
336
+ </Text>
337
+ <Controller
338
+ name="description"
339
+ control={control}
340
+ render={({ field }) => (
341
+ <Textarea
342
+ {...field}
343
+ value={field.value || ""}
344
+ placeholder={t("components.forms.EventForm.eventDescription")}
345
+ />
346
+ )}
347
+ />
348
+
349
+ <Text size="sm" fw={500}>
350
+ {t("placeholder.tags")}
351
+ </Text>
352
+ <Controller
353
+ name="tags"
354
+ control={control}
355
+ render={({ field }) => (
356
+ <TagsInput
357
+ {...field}
358
+ value={field.value || []}
359
+ placeholder={t("placeholder.tags")}
360
+ />
361
+ )}
362
+ />
363
+
364
+ {children}
365
+
366
+ <Divider my="sm" />
367
+
368
+ <UnstyledButton onClick={() => setAttributesOpen((o) => !o)}>
369
+ <Group justify="space-between">
370
+ <Title order={5}>{t("placeholder.attributes")}</Title>
371
+ <ChevronDownIcon
372
+ style={{
373
+ width: 16,
374
+ transform: attributesOpen
375
+ ? "rotate(180deg)"
376
+ : "rotate(0deg)",
377
+ transition: "transform 200ms ease",
378
+ }}
379
+ />
380
+ </Group>
381
+ </UnstyledButton>
382
+
383
+ <Collapse in={attributesOpen}>
384
+ <Controller
385
+ name="attributes"
386
+ control={control}
387
+ render={({ field }) => (
388
+ <EditableAttributes
389
+ {...field}
390
+ value={field.value || {}}
391
+ isEditing={true}
392
+ />
393
+ )}
394
+ />
395
+ </Collapse>
396
+ </Stack>
397
+ );
398
+ }}
399
+ </BaseForm>
400
+ );
401
+ };
@@ -0,0 +1,54 @@
1
+ import { Group } from "@mantine/core";
2
+ import { type Event } from "omni-osint-crud-client";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Controller, useFormContext, useWatch } from "react-hook-form";
5
+ import {
6
+ EventIconSelector,
7
+ EventColorSelector,
8
+ } from "@omnsight/osint-entity-components/icons";
9
+
10
+ export const IconFormSection = ({ data }: { data: Event }) => {
11
+ const {
12
+ control,
13
+ formState: { errors },
14
+ } = useFormContext<Event>();
15
+ const { t } = useTranslation();
16
+ const type = useWatch({ control, name: "type" });
17
+ const iconColor = useWatch({ control, name: "attributes.icon_color" });
18
+
19
+ const modifiedData = {
20
+ ...data,
21
+ type: type,
22
+ attributes: { ...data.attributes, icon_color: iconColor },
23
+ };
24
+
25
+ return (
26
+ <Group grow>
27
+ <Controller
28
+ name="type"
29
+ control={control}
30
+ rules={{ required: t("common.required") }}
31
+ render={({ field }) => (
32
+ <EventIconSelector
33
+ {...field}
34
+ data={modifiedData}
35
+ value={field.value}
36
+ error={errors.type?.message}
37
+ />
38
+ )}
39
+ />
40
+ <Controller
41
+ name="attributes.icon_color"
42
+ control={control}
43
+ rules={{ required: t("common.required") }}
44
+ render={({ field }) => (
45
+ <EventColorSelector
46
+ {...field}
47
+ value={field.value as string | undefined}
48
+ error={errors.attributes?.icon_color?.message}
49
+ />
50
+ )}
51
+ />
52
+ </Group>
53
+ );
54
+ };