@olenbetong/synergi-react 2.3.1 → 2.3.3

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 (28) hide show
  1. package/es/elements/ElementDamage/DescriptionInput.d.ts +3 -1
  2. package/es/elements/ElementDamage/DescriptionInput.js +10 -10
  3. package/es/elements/ElementDamage/DuplicateDamage/DesktopView.d.ts +2 -1
  4. package/es/elements/ElementDamage/DuplicateDamage/DesktopView.js +3 -3
  5. package/es/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.d.ts +2 -1
  6. package/es/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.js +2 -2
  7. package/es/elements/ElementDamage/DuplicateDamage/PhoneView.d.ts +2 -1
  8. package/es/elements/ElementDamage/DuplicateDamage/PhoneView.js +4 -2
  9. package/es/elements/ElementDamage/DuplicateDamage/index.d.ts +4 -0
  10. package/es/elements/ElementDamage/DuplicateDamage/index.js +5 -2
  11. package/es/elements/ElementLookup.d.ts +3 -1
  12. package/es/elements/ElementLookup.js +15 -4
  13. package/es/elements/ElementLookupDialog/Manual/ManualForm.d.ts +2 -1
  14. package/es/elements/ElementLookupDialog/Manual/ManualForm.js +41 -18
  15. package/es/elements/ElementLookupDialog/index.d.ts +1 -0
  16. package/es/elements/ElementLookupDialog/index.js +2 -2
  17. package/es/elements/index.d.ts +1 -0
  18. package/es/elements/index.js +1 -0
  19. package/package.json +4 -2
  20. package/src/elements/ElementDamage/DescriptionInput.tsx +19 -10
  21. package/src/elements/ElementDamage/DuplicateDamage/DesktopView.tsx +15 -10
  22. package/src/elements/ElementDamage/DuplicateDamage/DublicateElementDamageDialog.tsx +4 -2
  23. package/src/elements/ElementDamage/DuplicateDamage/PhoneView.tsx +5 -2
  24. package/src/elements/ElementDamage/DuplicateDamage/index.tsx +10 -1
  25. package/src/elements/ElementLookup.tsx +29 -3
  26. package/src/elements/ElementLookupDialog/Manual/ManualForm.tsx +52 -21
  27. package/src/elements/ElementLookupDialog/index.tsx +9 -2
  28. package/src/elements/index.ts +1 -0
@@ -7,8 +7,10 @@ export type DescriptionInputType = {
7
7
  type DescriptionInputProps = {
8
8
  value?: DescriptionInputType | null;
9
9
  defaultValue?: DescriptionInputType | null;
10
+ descriptionOptions?: string[];
11
+ storageNamespace?: string;
10
12
  onCopyPrevDamage?: (value: DescriptionInputType) => void;
11
13
  onChange?: (value: DescriptionInputType) => void;
12
14
  };
13
- export declare function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChange }: DescriptionInputProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function DescriptionInput({ value, defaultValue, descriptionOptions, storageNamespace, onCopyPrevDamage, onChange, }: DescriptionInputProps): import("react/jsx-runtime").JSX.Element;
14
16
  export {};
@@ -2,11 +2,15 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Autocomplete, Button, TextField } from "@mui/material";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
4
  import { useEffect, useRef, useState } from "react";
5
- export function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChange }) {
5
+ export function DescriptionInput({ value, defaultValue, descriptionOptions = [
6
+ getLocalizedString("Tear in element"),
7
+ getLocalizedString("Damage around lifting anchor"),
8
+ getLocalizedString("Damage on underside"),
9
+ ], storageNamespace = "ElementDamage", onCopyPrevDamage, onChange, }) {
6
10
  let initializedRef = useRef(false);
7
11
  let [input, setInput] = useState(defaultValue ?? null);
8
12
  let inputValue = value === undefined ? input : value;
9
- // biome-ignore lint: intentionally skipping "value" dependency as this should never run when "value" changes
13
+ // biome-ignore lint: intentionally skipping "value" dependency as this shouldn't run when "value" changes
10
14
  useEffect(() => {
11
15
  if (initializedRef.current) {
12
16
  return;
@@ -17,9 +21,9 @@ export function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChan
17
21
  }
18
22
  }, [defaultValue]);
19
23
  function handleCopyPrevDamage() {
20
- let description = localStorage.getItem("ElementDamage_Description") ?? "";
21
- let cause = localStorage.getItem("ElementDamage_Cause") ?? "";
22
- let action = localStorage.getItem("ElementDamage_Action") ?? "";
24
+ let description = localStorage.getItem(`${storageNamespace}_Description`) ?? "";
25
+ let cause = localStorage.getItem(`${storageNamespace}_Cause`) ?? "";
26
+ let action = localStorage.getItem(`${storageNamespace}_Action`) ?? "";
23
27
  onCopyPrevDamage?.({
24
28
  Description: description,
25
29
  Cause: cause,
@@ -36,9 +40,5 @@ export function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChan
36
40
  onChange?.(value);
37
41
  setInput(value);
38
42
  }
39
- return (_jsxs(_Fragment, { children: [_jsx(TextField, { name: "Date", type: "date", value: (inputValue?.Date ?? new Date()).toISOString().split("T")[0], onChange: (event) => handleOnChange({ ...inputValue, Date: new Date(event.target.value) }), id: "NewElementDamage_Date", label: getLocalizedString("Date"), fullWidth: true, required: true, slotProps: { inputLabel: { shrink: true } } }), _jsx(Button, { variant: "contained", onClick: handleCopyPrevDamage, children: getLocalizedString("Copy previous damage") }), _jsx(Autocomplete, { value: inputValue?.Description ?? "", onChange: (_event, value) => handleOnChange({ ...inputValue, Description: value ?? "" }), options: [
40
- getLocalizedString("Tear in element"),
41
- getLocalizedString("Damage around lifting anchor"),
42
- getLocalizedString("Damage on underside"),
43
- ], freeSolo: true, renderInput: (params) => (_jsx(TextField, { name: "Description", required: true, ...params, label: getLocalizedString("Description") })) }), _jsx(TextField, { name: "Cause", id: "NewElementDamage_Cause", value: inputValue?.Cause ?? "", onChange: (event) => handleOnChange({ ...inputValue, Cause: event.currentTarget.value }), label: getLocalizedString("Cause"), fullWidth: true }), _jsx(TextField, { name: "Action", id: "NewElementDamage_Action", value: inputValue?.Action ?? "", onChange: (event) => handleOnChange({ ...inputValue, Action: event.currentTarget.value }), label: getLocalizedString("Action"), fullWidth: true })] }));
43
+ return (_jsxs(_Fragment, { children: [_jsx(TextField, { name: "Date", type: "date", value: (inputValue?.Date ?? new Date()).toISOString().split("T")[0], onChange: (event) => handleOnChange({ ...inputValue, Date: new Date(event.target.value) }), id: "NewElementDamage_Date", label: getLocalizedString("Date"), fullWidth: true, required: true, slotProps: { inputLabel: { shrink: true } } }), _jsx(Button, { variant: "contained", onClick: handleCopyPrevDamage, children: getLocalizedString("Copy previous damage") }), _jsx(Autocomplete, { value: inputValue?.Description ?? "", onChange: (_event, value) => handleOnChange({ ...inputValue, Description: value ?? "" }), options: descriptionOptions, freeSolo: true, renderInput: (params) => (_jsx(TextField, { name: "Description", required: true, ...params, label: getLocalizedString("Description") })) }), _jsx(TextField, { name: "Cause", id: "NewElementDamage_Cause", value: inputValue?.Cause ?? "", onChange: (event) => handleOnChange({ ...inputValue, Cause: event.currentTarget.value }), label: getLocalizedString("Cause"), fullWidth: true }), _jsx(TextField, { name: "Action", id: "NewElementDamage_Action", value: inputValue?.Action ?? "", onChange: (event) => handleOnChange({ ...inputValue, Action: event.currentTarget.value }), label: getLocalizedString("Action"), fullWidth: true })] }));
44
44
  }
@@ -1,7 +1,8 @@
1
1
  import type { DamagesRecord } from "../../data";
2
2
  type ElementDamagesItemProps = {
3
3
  record: DamagesRecord;
4
+ linkToDamage: boolean;
4
5
  };
5
- export declare function ElementDamagesItem({ record }: ElementDamagesItemProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function ElementDamagesItem({ record, linkToDamage }: ElementDamagesItemProps): import("react/jsx-runtime").JSX.Element;
6
7
  export declare function ElementDamagesHeader(): import("react/jsx-runtime").JSX.Element;
7
8
  export {};
@@ -5,11 +5,11 @@ import { Link } from "@olenbetong/appframe-mui";
5
5
  function SummaryItem({ col, label, children, em, align, padding }) {
6
6
  return (_jsxs(Grid, { size: col, sx: { p: padding ?? 2, borderBottom: (t) => `1px solid ${t.palette.divider}` }, children: [label && (_jsx(Typography, { variant: "body1", fontSize: "0.9rem", fontWeight: 500, mb: 1, textAlign: align, children: getLocalizedString(label) })), children && (_jsx(Typography, { fontSize: em ? "1.4rem" : "0.9rem", whiteSpace: "pre-line", fontWeight: em ? 600 : undefined, textAlign: align, children: children }))] }));
7
7
  }
8
- export function ElementDamagesItem({ record }) {
9
- return (_jsxs(_Fragment, { children: [_jsx(SummaryItem, { col: 1.5, children: _jsx(Link, { href: "#", onClick: (e) => {
8
+ export function ElementDamagesItem({ record, linkToDamage }) {
9
+ return (_jsxs(_Fragment, { children: [_jsx(SummaryItem, { col: 1.5, children: linkToDamage ? (_jsx(Link, { href: "#", onClick: (e) => {
10
10
  e.preventDefault();
11
11
  window.location.href = `${record.ID}`;
12
- }, children: record.ID }) }), _jsx(SummaryItem, { col: 2.5, align: "right", children: record.Date.toISOString().split("T")[0] }), _jsx(SummaryItem, { col: 3, children: record.Description }), _jsx(SummaryItem, { col: 2.5, children: record.Cause }), _jsx(SummaryItem, { col: 2.5, children: record.Action })] }));
12
+ }, children: record.ID })) : (_jsx("span", { children: record.ID })) }), _jsx(SummaryItem, { col: 2.5, align: "right", children: record.Date.toISOString().split("T")[0] }), _jsx(SummaryItem, { col: 3, children: record.Description }), _jsx(SummaryItem, { col: 2.5, children: record.Cause }), _jsx(SummaryItem, { col: 2.5, children: record.Action })] }));
13
13
  }
14
14
  export function ElementDamagesHeader() {
15
15
  return (_jsxs(_Fragment, { children: [_jsx(SummaryItem, { col: 1.5, label: "ID" }), _jsx(SummaryItem, { col: 2.5, label: "Date", align: "right" }), _jsx(SummaryItem, { col: 3, label: "Description" }), _jsx(SummaryItem, { col: 2.5, label: "Cause" }), _jsx(SummaryItem, { col: 2.5, label: "Action" })] }));
@@ -5,6 +5,7 @@ type DuplicateElementDamageDialogProps = {
5
5
  record: DamagesRecord[];
6
6
  onRegisterAnyway: () => void;
7
7
  loading: boolean;
8
+ linkToDamage: boolean;
8
9
  };
9
- export declare function DuplicateElementDamageDialog({ open, onClose, record, onRegisterAnyway, loading, }: DuplicateElementDamageDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function DuplicateElementDamageDialog({ open, onClose, record, onRegisterAnyway, loading, linkToDamage, }: DuplicateElementDamageDialogProps): import("react/jsx-runtime").JSX.Element;
10
11
  export {};
@@ -5,8 +5,8 @@ import { getLocalizedString } from "@olenbetong/appframe-core";
5
5
  import { theme } from "@olenbetong/appframe-mui";
6
6
  import { ElementDamagesHeader, ElementDamagesItem } from "./DesktopView";
7
7
  import { DuplicatePhoneViewItem } from "./PhoneView";
8
- export function DuplicateElementDamageDialog({ open, onClose, record, onRegisterAnyway, loading, }) {
8
+ export function DuplicateElementDamageDialog({ open, onClose, record, onRegisterAnyway, loading, linkToDamage, }) {
9
9
  let matches = useMediaQuery(theme.breakpoints.up("lg"));
10
10
  let backgroundColor = "#e9ebee";
11
- return (_jsxs(Dialog, { open: open, onClose: onClose, fullWidth: true, fullScreen: !matches, children: [_jsx(DialogTitle, { sx: { backgroundColor: backgroundColor }, children: getLocalizedString("This element has one or more unfixed registered damage") }), _jsx(DialogContent, { sx: { backgroundColor: backgroundColor }, children: _jsx(Grid, { container: true, children: matches ? (_jsxs(_Fragment, { children: [_jsx(ElementDamagesHeader, {}), record.map((record) => (_jsx(ElementDamagesItem, { record: record }, record.PrimKey)))] })) : (_jsx("div", { className: "flow", style: { width: "100%" }, children: record.map((record) => (_jsx(DuplicatePhoneViewItem, { record: record }, record.PrimKey))) })) }) }), _jsxs(DialogActions, { sx: { backgroundColor: backgroundColor }, children: [_jsx(Button, { onClick: onClose, children: getLocalizedString("Cancel") }), _jsx(Button, { startIcon: _jsx(Add, {}), variant: "contained", type: "submit", onClick: onRegisterAnyway, loading: loading, loadingPosition: "start", children: getLocalizedString("Register damage anyway") })] })] }));
11
+ return (_jsxs(Dialog, { open: open, onClose: onClose, fullWidth: true, fullScreen: !matches, children: [_jsx(DialogTitle, { sx: { backgroundColor: backgroundColor }, children: getLocalizedString("This element has one or more unfixed registered damage") }), _jsx(DialogContent, { sx: { backgroundColor: backgroundColor }, children: _jsx(Grid, { container: true, children: matches ? (_jsxs(_Fragment, { children: [_jsx(ElementDamagesHeader, {}), record.map((record) => (_jsx(ElementDamagesItem, { record: record, linkToDamage: linkToDamage }, record.PrimKey)))] })) : (_jsx("div", { className: "flow", style: { width: "100%" }, children: record.map((record) => (_jsx(DuplicatePhoneViewItem, { record: record, linkToDamage: linkToDamage }, record.PrimKey))) })) }) }), _jsxs(DialogActions, { sx: { backgroundColor: backgroundColor }, children: [_jsx(Button, { onClick: onClose, children: getLocalizedString("Cancel") }), _jsx(Button, { startIcon: _jsx(Add, {}), variant: "contained", type: "submit", onClick: onRegisterAnyway, loading: loading, loadingPosition: "start", children: getLocalizedString("Register damage anyway") })] })] }));
12
12
  }
@@ -1,6 +1,7 @@
1
1
  import type { DamagesRecord } from "../../data";
2
2
  type DuplicatePhoneViewItemProps = {
3
3
  record: DamagesRecord;
4
+ linkToDamage: boolean;
4
5
  };
5
- export declare function DuplicatePhoneViewItem({ record }: DuplicatePhoneViewItemProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function DuplicatePhoneViewItem({ record, linkToDamage }: DuplicatePhoneViewItemProps): import("react/jsx-runtime").JSX.Element;
6
7
  export {};
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Card, CardContent, Typography } from "@mui/material";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
- export function DuplicatePhoneViewItem({ record }) {
4
+ export function DuplicatePhoneViewItem({ record, linkToDamage }) {
5
5
  return (_jsx(Card, { variant: "outlined", onClick: () => {
6
- window.location.href = `${record.ID}`;
6
+ if (linkToDamage) {
7
+ window.location.href = `${record.ID}`;
8
+ }
7
9
  }, children: _jsxs(CardContent, { children: [_jsx(Typography, { sx: { fontSize: 14 }, color: "text.secondary", gutterBottom: true, children: record.Date.toLocaleDateString() }), _jsxs(Typography, { variant: "h5", mb: 1.5, children: [record.ElementID, " - ", record.DisplayName] }), _jsxs(Typography, { children: [getLocalizedString("Description"), ":"] }), _jsx(Typography, { mb: 1.5, color: "text.secondary", children: record.Description }), _jsxs(Typography, { children: [getLocalizedString("Cause"), ": "] }), _jsx(Typography, { mb: 1.5, color: "text.secondary", children: record.Cause }), _jsxs(Typography, { children: [getLocalizedString("Action"), ": "] }), _jsx(Typography, { mb: 1.5, color: "text.secondary", children: record.Action })] }) }));
8
10
  }
@@ -21,6 +21,10 @@ type DuplicateElementDamageProps = {
21
21
  * Loading state for submit button
22
22
  */
23
23
  loading?: boolean;
24
+ /**
25
+ * Indicate if a link to the element damage form should be used
26
+ */
27
+ linkToDamage?: boolean;
24
28
  /**
25
29
  * Called when duplicate or duplicates are found
26
30
  * @param record Array with all of the duplicate damages
@@ -4,7 +4,7 @@ import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
4
4
  import { dsDamagesDuplicateCheck } from "../../data";
5
5
  import { DuplicateElementDamageDialog } from "./DublicateElementDamageDialog";
6
6
  export { DuplicateElementDamageDialog } from "./DublicateElementDamageDialog";
7
- export const DuplicateElementDamage = forwardRef(function DuplicateElementDamage({ element, open: controlledOpen, onOpenChange, disableCheck = false, loading = false, onRegisterAnyway, }, ref) {
7
+ export const DuplicateElementDamage = forwardRef(function DuplicateElementDamage({ element, open: controlledOpen, onOpenChange, disableCheck = false, loading = false, linkToDamage = true, onRegisterAnyway, }, ref) {
8
8
  let [record, setRecord] = useState([]);
9
9
  let [uncontrolledOpen, setUncontrolledOpen] = useState(false);
10
10
  let open = controlledOpen === undefined ? uncontrolledOpen : controlledOpen;
@@ -36,5 +36,8 @@ export const DuplicateElementDamage = forwardRef(function DuplicateElementDamage
36
36
  checkDuplicate: () => runDuplicateCheck(),
37
37
  record: record,
38
38
  }));
39
- return (_jsx(DuplicateElementDamageDialog, { open: open, onClose: () => handleOpenChange(false), record: record, onRegisterAnyway: onRegisterAnyway ?? (() => { }), loading: loading ?? false }));
39
+ return (_jsx(DuplicateElementDamageDialog, { open: open, onClose: () => {
40
+ setRecord([]);
41
+ handleOpenChange(false);
42
+ }, record: record, onRegisterAnyway: onRegisterAnyway ?? (() => { }), loading: loading ?? false, linkToDamage: linkToDamage }));
40
43
  });
@@ -14,7 +14,9 @@ export type ElementLookupProps = {
14
14
  value?: Element | null;
15
15
  defaultValue?: Element | null;
16
16
  ref?: RefObject<ElementLookupHandle | null>;
17
+ elementIDRequired?: boolean;
17
18
  openLookupDialogIcon?: boolean;
18
19
  onChange?: (value: Element | null) => void;
20
+ onDialogChange?: (open: boolean) => void;
19
21
  };
20
- export declare function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon, onChange }: ElementLookupProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function ElementLookup({ value, defaultValue, ref, elementIDRequired, openLookupDialogIcon, onChange, onDialogChange, }: ElementLookupProps): import("react/jsx-runtime").JSX.Element;
@@ -2,17 +2,23 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import SearchIcon from "@mui/icons-material/Search";
3
3
  import { IconButton, TextField } from "@mui/material";
4
4
  import { getLocalizedString } from "@olenbetong/appframe-core";
5
- import { useEffect, useImperativeHandle, useState } from "react";
5
+ import { useEffect, useImperativeHandle, useRef, useState } from "react";
6
6
  import { ElementLookupDialog } from "./ElementLookupDialog";
7
- export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon, onChange }) {
7
+ export function ElementLookup({ value, defaultValue, ref, elementIDRequired = false, openLookupDialogIcon, onChange, onDialogChange, }) {
8
+ let initializedRef = useRef(false);
8
9
  let [openElementLookupDialog, setOpenElementLookupDialog] = useState(false);
9
10
  let [element, setElement] = useState(defaultValue ?? null);
10
11
  let elementValue = value === undefined ? element : value;
12
+ // biome-ignore lint: intentionally skipping "value" dependency as this shouldn't run when "value" changes
11
13
  useEffect(() => {
14
+ if (initializedRef.current) {
15
+ return;
16
+ }
12
17
  if (value === undefined && defaultValue) {
13
18
  setElement(defaultValue);
19
+ initializedRef.current = true;
14
20
  }
15
- }, [defaultValue, value]);
21
+ }, [defaultValue]);
16
22
  useImperativeHandle(ref, () => ({
17
23
  openDialog: () => setOpenElementLookupDialog(true),
18
24
  closeDialog: () => setOpenElementLookupDialog(false),
@@ -21,10 +27,15 @@ export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon,
21
27
  onChange?.(addElement.element);
22
28
  setElement(addElement.element);
23
29
  }
24
- return (_jsxs(_Fragment, { children: [_jsx(ElementLookupDialog, { open: openElementLookupDialog, mode: "single", onClose: () => setOpenElementLookupDialog(false), onAdd: handleAddElement }), _jsx("input", { type: "hidden", name: "ProjectID", value: elementValue?.ProjectID ?? "" }), _jsx("input", { type: "hidden", name: "ElementRef", value: elementValue?.PrimKey ?? "" }), _jsx(TextField, { id: "NewElementDamge_Project", value: elementValue ? `${elementValue.ProjectID}: ${elementValue.ProjectName}` : "", label: getLocalizedString("Project"), fullWidth: true, required: true, autoComplete: "off", slotProps: { htmlInput: { readOnly: true } } }), _jsx(TextField, { name: "ElementID", id: "NewElementDamage_ElementID", value: elementValue ? elementValue.ElementID : "", label: getLocalizedString("Element ID"), fullWidth: true, autoComplete: "off", slotProps: {
30
+ // biome-ignore lint: only run when openElementLookupDialog changes
31
+ useEffect(() => {
32
+ onDialogChange?.(openElementLookupDialog);
33
+ }, [openElementLookupDialog]);
34
+ return (_jsxs(_Fragment, { children: [_jsx(ElementLookupDialog, { open: openElementLookupDialog, mode: "single", onClose: () => setOpenElementLookupDialog(false), onAdd: handleAddElement, allowEmptyElement: !elementIDRequired }), _jsx("input", { type: "hidden", name: "ProjectID", value: elementValue?.ProjectID ?? "" }), _jsx("input", { type: "hidden", name: "ElementRef", value: elementValue?.PrimKey ?? "" }), _jsx(TextField, { id: "NewElementDamge_Project", value: elementValue ? `${elementValue.ProjectID}: ${elementValue.ProjectName}` : "", label: getLocalizedString("Project"), fullWidth: true, required: true, autoComplete: "off", slotProps: { htmlInput: { readOnly: true } } }), _jsx(TextField, { name: "ElementID", id: "NewElementDamage_ElementID", value: elementValue ? elementValue.ElementID : "", label: getLocalizedString("Element ID"), fullWidth: true, required: elementIDRequired, autoComplete: "off", slotProps: {
25
35
  htmlInput: { readOnly: true },
26
36
  input: {
27
37
  endAdornment: openLookupDialogIcon ? (_jsx(IconButton, { onClick: () => setOpenElementLookupDialog(true), children: _jsx(SearchIcon, {}) })) : undefined,
28
38
  },
39
+ inputLabel: { shrink: !!elementValue?.ElementID },
29
40
  } })] }));
30
41
  }
@@ -8,5 +8,6 @@ export type AddElementManualFormProps = {
8
8
  onAdd: (element: string | ElementDetails) => Promise<void>;
9
9
  mode?: "single" | "multi";
10
10
  defaultProject?: ProjectRecordBase;
11
+ allowEmptyElement?: boolean;
11
12
  };
12
- export declare function AddElementManualForm({ onAdd, mode, defaultProject }: AddElementManualFormProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function AddElementManualForm({ onAdd, mode, defaultProject, allowEmptyElement, }: AddElementManualFormProps): import("react/jsx-runtime").JSX.Element;
@@ -7,7 +7,7 @@ import { ProjectSelect } from "../../ProjectSelect";
7
7
  import { getElement } from "../getElement";
8
8
  const isElementsOnly = /^([a-z]{2,4}[\s]*[0-9]{4,8}[\s,]*)+$/i;
9
9
  const matchElements = /([a-z]{2,4}[\s]*[0-9]{4,8})/gi;
10
- export function AddElementManualForm({ onAdd, mode, defaultProject }) {
10
+ export function AddElementManualForm({ onAdd, mode, defaultProject, allowEmptyElement = false, }) {
11
11
  let id = useId();
12
12
  let buttonRef = useRef(null);
13
13
  let [working, setWorking] = useState(false);
@@ -20,29 +20,52 @@ export function AddElementManualForm({ onAdd, mode, defaultProject }) {
20
20
  let formData = new FormData(evt.target);
21
21
  let elements = formData.get("ElementID");
22
22
  let domain = formData.get("Domain");
23
- let projectId = formData.get("ProjectID");
24
- if (!projectId || !domain || !elements) {
25
- let warning = getLocalizedString(mode === "multi" ? "Project and elements are required" : "Project and element are required");
26
- alert(warning);
27
- return;
23
+ let project = JSON.parse(formData.get("Project"));
24
+ if (allowEmptyElement) {
25
+ if (!project.ProjectID) {
26
+ alert(getLocalizedString("Project is required"));
27
+ return;
28
+ }
28
29
  }
29
- setWorking(true);
30
- let record = elements
31
- .match(matchElements)
32
- ?.map((elementId) => elementId.replace(/\s+/g, ""))
33
- .map((elementId) => getElement({ projectId: projectId, elementId: elementId }, { mode: "manual" }));
34
- if (!record) {
35
- alert(getLocalizedString("Invalid elements"));
36
- return;
30
+ else {
31
+ if (!project.ProjectID || !domain || !elements) {
32
+ let warning = getLocalizedString(mode === "multi" ? "Project and elements are required" : "Project and element are required");
33
+ alert(warning);
34
+ return;
35
+ }
36
+ }
37
+ if (elements) {
38
+ setWorking(true);
39
+ let record = elements
40
+ .match(matchElements)
41
+ ?.map((elementId) => elementId.replace(/\s+/g, ""))
42
+ .map((elementId) => getElement({ projectId: project.ProjectID, elementId: elementId }, { mode: "manual" }));
43
+ if (!record) {
44
+ alert(getLocalizedString("Invalid elements"));
45
+ return;
46
+ }
47
+ let loaded = (await Promise.all(record)).map((record) => onAdd(record));
48
+ await Promise.all(loaded);
49
+ setWorking(false);
50
+ }
51
+ else {
52
+ onAdd({
53
+ PrimKey: "",
54
+ Domain: domain?.toString() ?? "",
55
+ Created: project?.Created,
56
+ ProjectID: project.ProjectID,
57
+ ProjectName: project.ProjectName,
58
+ ProjectDomain: project.Domain,
59
+ ElementID: "",
60
+ DisplayName: "",
61
+ GiaiUri: "",
62
+ });
37
63
  }
38
- let loaded = (await Promise.all(record)).map((record) => onAdd(record));
39
- await Promise.all(loaded);
40
- setWorking(false);
41
64
  }
42
65
  catch (error) {
43
66
  setWorking(false);
44
67
  alert(error.message);
45
68
  }
46
69
  }
47
- return (_jsxs("form", { id: id, onSubmit: handleSubmit, method: "post", children: [_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: getLocalizedString("Manually entering elements is a major source of errors. Only use this as a last resort. If the camera or scanner is not working, please contact the IT department.") }), _jsx(ProjectSelect, { onChange: (project) => (project ? setProject(project) : setProject(null)), value: project, id: "AddElement_Project" }), _jsx(TextField, { variant: "filled", fullWidth: true, autoComplete: "off", margin: "normal", name: "ElementID", id: "AddElement_ElementID", label: getLocalizedString("Element ID"), value: elementId, onChange: (evt) => setElementId(evt.target.value) }), _jsx(Button, { form: id, ref: buttonRef, color: "primary", variant: "contained", size: "large", type: "submit", disabled: !isElementsOnly.test(elementId) || working, loading: working, loadingPosition: "end", endIcon: _jsx(Add, {}), children: getLocalizedString("Add") })] }));
70
+ return (_jsxs("form", { id: id, onSubmit: handleSubmit, method: "post", children: [_jsx(Alert, { severity: "warning", sx: { mb: 2 }, children: getLocalizedString("Manually entering elements is a major source of errors. Only use this as a last resort. If the camera or scanner is not working, please contact the IT department.") }), _jsx("input", { type: "hidden", name: "Project", value: project ? JSON.stringify(project) : "" }), _jsx(ProjectSelect, { onChange: (project) => (project ? setProject(project) : setProject(null)), value: project, id: "AddElement_Project" }), _jsx(TextField, { variant: "filled", fullWidth: true, autoComplete: "off", margin: "normal", name: "ElementID", id: "AddElement_ElementID", label: getLocalizedString("Element ID"), value: elementId, onChange: (evt) => setElementId(evt.target.value) }), _jsx(Button, { form: id, ref: buttonRef, color: "primary", variant: "contained", size: "large", type: "submit", disabled: (!allowEmptyElement && !isElementsOnly.test(elementId)) || working, loading: working, loadingPosition: "end", endIcon: _jsx(Add, {}), children: getLocalizedString("Add") })] }));
48
71
  }
@@ -10,6 +10,7 @@ export type ElementLookupDialogSharedProps = {
10
10
  onClose: () => void;
11
11
  initialTab?: InitialTabProps | null;
12
12
  useSearch?: boolean;
13
+ allowEmptyElement?: boolean;
13
14
  };
14
15
  export type ElementLookupDialogSingleModeProps = ElementLookupDialogSharedProps & {
15
16
  mode: "single" | "search";
@@ -72,11 +72,11 @@ export function ElementLookupDialog(props) {
72
72
  }
73
73
  return (_jsx(ResponsiveDialog, { open: open, onClose: onClose, slotProps: {
74
74
  paper: { sx: { overflow: "hidden" } },
75
- }, children: _jsxs(Box, { display: "flex", flexDirection: "column", maxHeight: "100%", overflow: "hidden", flexGrow: 1, children: [_jsxs(Tabs, { value: tab, onChange: (_, newTab) => setTab(newTab), variant: "fullWidth", sx: { flexShrink: 0 }, children: [props.mode !== "search" && _jsx(Tab, { icon: _jsx(Edit, {}), label: getLocalizedString("Manual"), value: "manual" }), _jsx(Tab, { icon: _jsx(TapAndPlay, {}), label: getLocalizedString("Scanner"), value: "scanner" }), _jsx(Tab, { icon: _jsx(QrCodeScanner, {}), label: getLocalizedString("Camera"), value: "camera" })] }), tab === "camera" && (_jsx(Box, { flexBasis: "12rem", flexGrow: 1, flexShrink: 2, children: _jsx(BarcodeScanner, { onRead: (element) => handleElementAdd(element, "camera-barcode") }) })), tab === "manual" && (_jsx(Box, { p: 2, children: _jsx(AddElementManualForm, { onAdd: (element) => handleElementAdd(element, "manual") }) })), tab === "scanner" && (_jsx(Box, { flexBasis: "12rem", flexGrow: 1, flexShrink: 2, children: _jsx(AddElementHIDScanner, { onScan: (element) => handleElementAdd(element, element.startsWith(Giai.BARCODE_PREFIX) ? "hid-barcode" : "hid-rfid") }) })), _jsxs(Box, { flex: "3 1 14rem", overflow: "auto", children: [props.mode === "multi" && (_jsx(Alert, { icon: false, severity: "info", sx: { justifyContent: "center", py: 0, position: "sticky", top: 0, zIndex: 1 }, slotProps: {
75
+ }, children: _jsxs(Box, { display: "flex", flexDirection: "column", maxHeight: "100%", overflow: "hidden", flexGrow: 1, children: [_jsxs(Tabs, { value: tab, onChange: (_, newTab) => setTab(newTab), variant: "fullWidth", sx: { flexShrink: 0 }, children: [props.mode !== "search" && _jsx(Tab, { icon: _jsx(Edit, {}), label: getLocalizedString("Manual"), value: "manual" }), _jsx(Tab, { icon: _jsx(TapAndPlay, {}), label: getLocalizedString("Scanner"), value: "scanner" }), _jsx(Tab, { icon: _jsx(QrCodeScanner, {}), label: getLocalizedString("Camera"), value: "camera" })] }), tab === "camera" && (_jsx(Box, { flexBasis: "12rem", flexGrow: 1, flexShrink: 2, children: _jsx(BarcodeScanner, { onRead: (element) => handleElementAdd(element, "camera-barcode") }) })), tab === "manual" && (_jsx(Box, { p: 2, children: _jsx(AddElementManualForm, { onAdd: (element) => handleElementAdd(element, "manual"), allowEmptyElement: props.allowEmptyElement }) })), tab === "scanner" && (_jsx(Box, { flexBasis: "12rem", flexGrow: 1, flexShrink: 2, children: _jsx(AddElementHIDScanner, { onScan: (element) => handleElementAdd(element, element.startsWith(Giai.BARCODE_PREFIX) ? "hid-barcode" : "hid-rfid") }) })), _jsxs(Box, { flex: "3 1 14rem", overflow: "auto", children: [props.mode === "multi" && (_jsx(Alert, { icon: false, severity: "info", sx: { justifyContent: "center", py: 0, position: "sticky", top: 0, zIndex: 1 }, slotProps: {
76
76
  message: { sx: { py: 0.5 } },
77
77
  }, children: localize `${elements.length} elements selected` })), _jsx(List, { disablePadding: true, children: elements.map(({ element }) => (_jsx(ListItem, { "data-elementid": element.ElementID, secondaryAction: _jsx(Button, { color: "error", variant: "outlined", onClick: () => {
78
78
  setElements((current) => props.mode === "multi" ? current.filter((e) => e.element.ElementID !== element.ElementID) : []);
79
- }, children: _jsx(DeleteForeverOutlined, {}) }), children: _jsx(ListItemText, { primary: element.DisplayName, secondary: element.ElementID }) }, element.ElementID))) })] }), _jsxs(DialogActions, { sx: { flexShrink: 0 }, children: [_jsx(Button, { onClick: onClose, children: getLocalizedString("Cancel") }), _jsx(Button, { variant: "contained", loading: adding, onClick: async () => {
79
+ }, children: _jsx(DeleteForeverOutlined, {}) }), children: _jsx(ListItemText, { primary: element.DisplayName || element.ProjectName, secondary: element.ElementID || element.ProjectID }) }, element.ElementID))) })] }), _jsxs(DialogActions, { sx: { flexShrink: 0 }, children: [_jsx(Button, { onClick: onClose, children: getLocalizedString("Cancel") }), _jsx(Button, { variant: "contained", loading: adding, onClick: async () => {
80
80
  try {
81
81
  setAdding(true);
82
82
  if (props.mode === "multi") {
@@ -7,3 +7,4 @@ export * from "./ElementDamage/RegisterFix.js";
7
7
  export * from "./ElementDamage/SubmitButtons.js";
8
8
  export * from "./ElementLookup.js";
9
9
  export * from "./ElementLookupDialog/index.js";
10
+ export * from "./ProjectSelect.js";
@@ -7,3 +7,4 @@ export * from "./ElementDamage/RegisterFix.js";
7
7
  export * from "./ElementDamage/SubmitButtons.js";
8
8
  export * from "./ElementLookup.js";
9
9
  export * from "./ElementLookupDialog/index.js";
10
+ export * from "./ProjectSelect.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@olenbetong/synergi-react",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "Standalone React component for SynergiWeb and Partner Portal",
5
5
  "type": "module",
6
6
  "exports": {
@@ -38,7 +38,9 @@
38
38
  "filepond-plugin-image-transform": "^3.8.7",
39
39
  "localforage": "^1.10.0",
40
40
  "mitt": "^3.0.1",
41
- "@olenbetong/appframe-core": "2.11.10"
41
+ "@olenbetong/appframe-core": "2.11.10",
42
+ "@olenbetong/appframe-mui": "6.0.7",
43
+ "@olenbetong/appframe-react": "1.21.21"
42
44
  },
43
45
  "repository": {
44
46
  "type": "git",
@@ -12,17 +12,30 @@ export type DescriptionInputType = {
12
12
  type DescriptionInputProps = {
13
13
  value?: DescriptionInputType | null;
14
14
  defaultValue?: DescriptionInputType | null;
15
+ descriptionOptions?: string[];
16
+ storageNamespace?: string;
15
17
  onCopyPrevDamage?: (value: DescriptionInputType) => void;
16
18
  onChange?: (value: DescriptionInputType) => void;
17
19
  };
18
20
 
19
- export function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChange }: DescriptionInputProps) {
21
+ export function DescriptionInput({
22
+ value,
23
+ defaultValue,
24
+ descriptionOptions = [
25
+ getLocalizedString("Tear in element"),
26
+ getLocalizedString("Damage around lifting anchor"),
27
+ getLocalizedString("Damage on underside"),
28
+ ],
29
+ storageNamespace = "ElementDamage",
30
+ onCopyPrevDamage,
31
+ onChange,
32
+ }: DescriptionInputProps) {
20
33
  let initializedRef = useRef<boolean>(false);
21
34
 
22
35
  let [input, setInput] = useState<DescriptionInputType | null>(defaultValue ?? null);
23
36
  let inputValue = value === undefined ? input : value;
24
37
 
25
- // biome-ignore lint: intentionally skipping "value" dependency as this should never run when "value" changes
38
+ // biome-ignore lint: intentionally skipping "value" dependency as this shouldn't run when "value" changes
26
39
  useEffect(() => {
27
40
  if (initializedRef.current) {
28
41
  return;
@@ -35,9 +48,9 @@ export function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChan
35
48
  }, [defaultValue]);
36
49
 
37
50
  function handleCopyPrevDamage() {
38
- let description = localStorage.getItem("ElementDamage_Description") ?? "";
39
- let cause = localStorage.getItem("ElementDamage_Cause") ?? "";
40
- let action = localStorage.getItem("ElementDamage_Action") ?? "";
51
+ let description = localStorage.getItem(`${storageNamespace}_Description`) ?? "";
52
+ let cause = localStorage.getItem(`${storageNamespace}_Cause`) ?? "";
53
+ let action = localStorage.getItem(`${storageNamespace}_Action`) ?? "";
41
54
 
42
55
  onCopyPrevDamage?.({
43
56
  Description: description,
@@ -78,11 +91,7 @@ export function DescriptionInput({ value, defaultValue, onCopyPrevDamage, onChan
78
91
  <Autocomplete
79
92
  value={inputValue?.Description ?? ""}
80
93
  onChange={(_event, value) => handleOnChange({ ...inputValue, Description: value ?? "" })}
81
- options={[
82
- getLocalizedString("Tear in element"),
83
- getLocalizedString("Damage around lifting anchor"),
84
- getLocalizedString("Damage on underside"),
85
- ]}
94
+ options={descriptionOptions}
86
95
  freeSolo
87
96
  renderInput={(params) => (
88
97
  <TextField name="Description" required {...(params as any)} label={getLocalizedString("Description")} />
@@ -36,21 +36,26 @@ function SummaryItem({ col, label, children, em, align, padding }: SummaryItemPr
36
36
 
37
37
  type ElementDamagesItemProps = {
38
38
  record: DamagesRecord;
39
+ linkToDamage: boolean;
39
40
  };
40
41
 
41
- export function ElementDamagesItem({ record }: ElementDamagesItemProps) {
42
+ export function ElementDamagesItem({ record, linkToDamage }: ElementDamagesItemProps) {
42
43
  return (
43
44
  <>
44
45
  <SummaryItem col={1.5}>
45
- <Link
46
- href="#"
47
- onClick={(e) => {
48
- e.preventDefault();
49
- window.location.href = `${record.ID}`;
50
- }}
51
- >
52
- {record.ID}
53
- </Link>
46
+ {linkToDamage ? (
47
+ <Link
48
+ href="#"
49
+ onClick={(e) => {
50
+ e.preventDefault();
51
+ window.location.href = `${record.ID}`;
52
+ }}
53
+ >
54
+ {record.ID}
55
+ </Link>
56
+ ) : (
57
+ <span>{record.ID}</span>
58
+ )}
54
59
  </SummaryItem>
55
60
  <SummaryItem col={2.5} align="right">
56
61
  {record.Date.toISOString().split("T")[0]}
@@ -12,6 +12,7 @@ type DuplicateElementDamageDialogProps = {
12
12
  record: DamagesRecord[];
13
13
  onRegisterAnyway: () => void;
14
14
  loading: boolean;
15
+ linkToDamage: boolean;
15
16
  };
16
17
 
17
18
  export function DuplicateElementDamageDialog({
@@ -20,6 +21,7 @@ export function DuplicateElementDamageDialog({
20
21
  record,
21
22
  onRegisterAnyway,
22
23
  loading,
24
+ linkToDamage,
23
25
  }: DuplicateElementDamageDialogProps) {
24
26
  let matches = useMediaQuery(theme.breakpoints.up("lg"));
25
27
  let backgroundColor = "#e9ebee";
@@ -35,13 +37,13 @@ export function DuplicateElementDamageDialog({
35
37
  <>
36
38
  <ElementDamagesHeader />
37
39
  {record.map((record) => (
38
- <ElementDamagesItem key={record.PrimKey} record={record} />
40
+ <ElementDamagesItem key={record.PrimKey} record={record} linkToDamage={linkToDamage} />
39
41
  ))}
40
42
  </>
41
43
  ) : (
42
44
  <div className="flow" style={{ width: "100%" }}>
43
45
  {record.map((record) => (
44
- <DuplicatePhoneViewItem key={record.PrimKey} record={record} />
46
+ <DuplicatePhoneViewItem key={record.PrimKey} record={record} linkToDamage={linkToDamage} />
45
47
  ))}
46
48
  </div>
47
49
  )}
@@ -4,14 +4,17 @@ import type { DamagesRecord } from "../../data";
4
4
 
5
5
  type DuplicatePhoneViewItemProps = {
6
6
  record: DamagesRecord;
7
+ linkToDamage: boolean;
7
8
  };
8
9
 
9
- export function DuplicatePhoneViewItem({ record }: DuplicatePhoneViewItemProps) {
10
+ export function DuplicatePhoneViewItem({ record, linkToDamage }: DuplicatePhoneViewItemProps) {
10
11
  return (
11
12
  <Card
12
13
  variant="outlined"
13
14
  onClick={() => {
14
- window.location.href = `${record.ID}`;
15
+ if (linkToDamage) {
16
+ window.location.href = `${record.ID}`;
17
+ }
15
18
  }}
16
19
  >
17
20
  <CardContent>
@@ -27,6 +27,10 @@ type DuplicateElementDamageProps = {
27
27
  * Loading state for submit button
28
28
  */
29
29
  loading?: boolean;
30
+ /**
31
+ * Indicate if a link to the element damage form should be used
32
+ */
33
+ linkToDamage?: boolean;
30
34
  /**
31
35
  * Called when duplicate or duplicates are found
32
36
  * @param record Array with all of the duplicate damages
@@ -48,6 +52,7 @@ export const DuplicateElementDamage = forwardRef<DuplicateElementDamageHandle, D
48
52
  onOpenChange,
49
53
  disableCheck = false,
50
54
  loading = false,
55
+ linkToDamage = true,
51
56
  onRegisterAnyway,
52
57
  }: DuplicateElementDamageProps,
53
58
  ref,
@@ -95,10 +100,14 @@ export const DuplicateElementDamage = forwardRef<DuplicateElementDamageHandle, D
95
100
  return (
96
101
  <DuplicateElementDamageDialog
97
102
  open={open}
98
- onClose={() => handleOpenChange(false)}
103
+ onClose={() => {
104
+ setRecord([]);
105
+ handleOpenChange(false);
106
+ }}
99
107
  record={record}
100
108
  onRegisterAnyway={onRegisterAnyway ?? (() => {})}
101
109
  loading={loading ?? false}
110
+ linkToDamage={linkToDamage}
102
111
  />
103
112
  );
104
113
  },
@@ -1,7 +1,7 @@
1
1
  import SearchIcon from "@mui/icons-material/Search";
2
2
  import { IconButton, TextField } from "@mui/material";
3
3
  import { getLocalizedString } from "@olenbetong/appframe-core";
4
- import { type RefObject, useEffect, useImperativeHandle, useState } from "react";
4
+ import { type RefObject, useEffect, useImperativeHandle, useRef, useState } from "react";
5
5
  import { type AddElement, ElementLookupDialog } from "./ElementLookupDialog";
6
6
 
7
7
  export type Element = {
@@ -21,20 +21,38 @@ export type ElementLookupProps = {
21
21
  value?: Element | null;
22
22
  defaultValue?: Element | null;
23
23
  ref?: RefObject<ElementLookupHandle | null>;
24
+ elementIDRequired?: boolean;
24
25
  openLookupDialogIcon?: boolean;
25
26
  onChange?: (value: Element | null) => void;
27
+ onDialogChange?: (open: boolean) => void;
26
28
  };
27
29
 
28
- export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon, onChange }: ElementLookupProps) {
30
+ export function ElementLookup({
31
+ value,
32
+ defaultValue,
33
+ ref,
34
+ elementIDRequired = false,
35
+ openLookupDialogIcon,
36
+ onChange,
37
+ onDialogChange,
38
+ }: ElementLookupProps) {
39
+ let initializedRef = useRef<boolean>(false);
40
+
29
41
  let [openElementLookupDialog, setOpenElementLookupDialog] = useState<boolean>(false);
30
42
  let [element, setElement] = useState<Element | null>(defaultValue ?? null);
31
43
  let elementValue = value === undefined ? element : value;
32
44
 
45
+ // biome-ignore lint: intentionally skipping "value" dependency as this shouldn't run when "value" changes
33
46
  useEffect(() => {
47
+ if (initializedRef.current) {
48
+ return;
49
+ }
50
+
34
51
  if (value === undefined && defaultValue) {
35
52
  setElement(defaultValue);
53
+ initializedRef.current = true;
36
54
  }
37
- }, [defaultValue, value]);
55
+ }, [defaultValue]);
38
56
 
39
57
  useImperativeHandle(ref, () => ({
40
58
  openDialog: () => setOpenElementLookupDialog(true),
@@ -46,6 +64,11 @@ export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon,
46
64
  setElement(addElement.element);
47
65
  }
48
66
 
67
+ // biome-ignore lint: only run when openElementLookupDialog changes
68
+ useEffect(() => {
69
+ onDialogChange?.(openElementLookupDialog);
70
+ }, [openElementLookupDialog]);
71
+
49
72
  return (
50
73
  <>
51
74
  <ElementLookupDialog
@@ -53,6 +76,7 @@ export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon,
53
76
  mode="single"
54
77
  onClose={() => setOpenElementLookupDialog(false)}
55
78
  onAdd={handleAddElement}
79
+ allowEmptyElement={!elementIDRequired}
56
80
  />
57
81
 
58
82
  <input type="hidden" name="ProjectID" value={elementValue?.ProjectID ?? ""} />
@@ -73,6 +97,7 @@ export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon,
73
97
  value={elementValue ? elementValue.ElementID : ""}
74
98
  label={getLocalizedString("Element ID")}
75
99
  fullWidth
100
+ required={elementIDRequired}
76
101
  autoComplete="off"
77
102
  slotProps={{
78
103
  htmlInput: { readOnly: true },
@@ -83,6 +108,7 @@ export function ElementLookup({ value, defaultValue, ref, openLookupDialogIcon,
83
108
  </IconButton>
84
109
  ) : undefined,
85
110
  },
111
+ inputLabel: { shrink: !!elementValue?.ElementID },
86
112
  }}
87
113
  />
88
114
  </>
@@ -19,9 +19,15 @@ export type AddElementManualFormProps = {
19
19
  onAdd: (element: string | ElementDetails) => Promise<void>;
20
20
  mode?: "single" | "multi";
21
21
  defaultProject?: ProjectRecordBase;
22
+ allowEmptyElement?: boolean;
22
23
  };
23
24
 
24
- export function AddElementManualForm({ onAdd, mode, defaultProject }: AddElementManualFormProps) {
25
+ export function AddElementManualForm({
26
+ onAdd,
27
+ mode,
28
+ defaultProject,
29
+ allowEmptyElement = false,
30
+ }: AddElementManualFormProps) {
25
31
  let id = useId();
26
32
  let buttonRef = useRef<HTMLButtonElement>(null);
27
33
  let [working, setWorking] = useState(false);
@@ -35,30 +41,53 @@ export function AddElementManualForm({ onAdd, mode, defaultProject }: AddElement
35
41
  let formData = new FormData(evt.target as HTMLFormElement);
36
42
  let elements = formData.get("ElementID");
37
43
  let domain = formData.get("Domain");
38
- let projectId = formData.get("ProjectID");
44
+ let project: ProjectsRecord = JSON.parse(formData.get("Project") as string);
39
45
 
40
- if (!projectId || !domain || !elements) {
41
- let warning = getLocalizedString(
42
- mode === "multi" ? "Project and elements are required" : "Project and element are required",
43
- );
44
- alert(warning);
45
- return;
46
+ if (allowEmptyElement) {
47
+ if (!project.ProjectID) {
48
+ alert(getLocalizedString("Project is required"));
49
+ return;
50
+ }
51
+ } else {
52
+ if (!project.ProjectID || !domain || !elements) {
53
+ let warning = getLocalizedString(
54
+ mode === "multi" ? "Project and elements are required" : "Project and element are required",
55
+ );
56
+ alert(warning);
57
+ return;
58
+ }
46
59
  }
47
60
 
48
- setWorking(true);
49
- let record = (elements as string)
50
- .match(matchElements)
51
- ?.map((elementId) => elementId.replace(/\s+/g, ""))
52
- .map((elementId) => getElement({ projectId: projectId as string, elementId: elementId }, { mode: "manual" }));
61
+ if (elements) {
62
+ setWorking(true);
63
+ let record = (elements as string)
64
+ .match(matchElements)
65
+ ?.map((elementId) => elementId.replace(/\s+/g, ""))
66
+ .map((elementId) =>
67
+ getElement({ projectId: project.ProjectID as string, elementId: elementId }, { mode: "manual" }),
68
+ );
53
69
 
54
- if (!record) {
55
- alert(getLocalizedString("Invalid elements"));
56
- return;
57
- }
70
+ if (!record) {
71
+ alert(getLocalizedString("Invalid elements"));
72
+ return;
73
+ }
58
74
 
59
- let loaded = (await Promise.all(record)).map((record) => onAdd(record));
60
- await Promise.all(loaded);
61
- setWorking(false);
75
+ let loaded = (await Promise.all(record)).map((record) => onAdd(record));
76
+ await Promise.all(loaded);
77
+ setWorking(false);
78
+ } else {
79
+ onAdd({
80
+ PrimKey: "",
81
+ Domain: domain?.toString() ?? "",
82
+ Created: project?.Created,
83
+ ProjectID: project.ProjectID,
84
+ ProjectName: project.ProjectName,
85
+ ProjectDomain: project.Domain,
86
+ ElementID: "",
87
+ DisplayName: "",
88
+ GiaiUri: "",
89
+ });
90
+ }
62
91
  } catch (error) {
63
92
  setWorking(false);
64
93
  alert((error as Error).message);
@@ -73,6 +102,8 @@ export function AddElementManualForm({ onAdd, mode, defaultProject }: AddElement
73
102
  )}
74
103
  </Alert>
75
104
 
105
+ <input type="hidden" name="Project" value={project ? JSON.stringify(project) : ""} />
106
+
76
107
  <ProjectSelect
77
108
  onChange={(project) => (project ? setProject(project) : setProject(null))}
78
109
  value={project}
@@ -98,7 +129,7 @@ export function AddElementManualForm({ onAdd, mode, defaultProject }: AddElement
98
129
  variant="contained"
99
130
  size="large"
100
131
  type="submit"
101
- disabled={!isElementsOnly.test(elementId) || working}
132
+ disabled={(!allowEmptyElement && !isElementsOnly.test(elementId)) || working}
102
133
  loading={working}
103
134
  loadingPosition="end"
104
135
  endIcon={<Add />}
@@ -28,6 +28,7 @@ export type ElementLookupDialogSharedProps = {
28
28
  onClose: () => void;
29
29
  initialTab?: InitialTabProps | null;
30
30
  useSearch?: boolean;
31
+ allowEmptyElement?: boolean;
31
32
  };
32
33
 
33
34
  export type ElementLookupDialogSingleModeProps = ElementLookupDialogSharedProps & {
@@ -128,7 +129,10 @@ export function ElementLookupDialog(props: ElementLookupDialogProps) {
128
129
  )}
129
130
  {tab === "manual" && (
130
131
  <Box p={2}>
131
- <AddElementManualForm onAdd={(element) => handleElementAdd(element, "manual")} />
132
+ <AddElementManualForm
133
+ onAdd={(element) => handleElementAdd(element, "manual")}
134
+ allowEmptyElement={props.allowEmptyElement}
135
+ />
132
136
  </Box>
133
137
  )}
134
138
  {tab === "scanner" && (
@@ -171,7 +175,10 @@ export function ElementLookupDialog(props: ElementLookupDialogProps) {
171
175
  </Button>
172
176
  }
173
177
  >
174
- <ListItemText primary={element.DisplayName} secondary={element.ElementID} />
178
+ <ListItemText
179
+ primary={element.DisplayName || element.ProjectName}
180
+ secondary={element.ElementID || element.ProjectID}
181
+ />
175
182
  </ListItem>
176
183
  ))}
177
184
  </List>
@@ -7,3 +7,4 @@ export * from "./ElementDamage/RegisterFix.js";
7
7
  export * from "./ElementDamage/SubmitButtons.js";
8
8
  export * from "./ElementLookup.js";
9
9
  export * from "./ElementLookupDialog/index.js";
10
+ export * from "./ProjectSelect.js";