@truedat/core 8.5.7 → 8.5.8

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 (25) hide show
  1. package/package.json +3 -3
  2. package/src/components/GroupActions.js +44 -3
  3. package/src/components/HistoryBackButton.js +1 -0
  4. package/src/components/LanguagesTabs.js +6 -3
  5. package/src/components/ResourceMembersActions.js +1 -0
  6. package/src/components/TemplateSelector.js +24 -5
  7. package/src/components/TreeSelector.js +15 -1
  8. package/src/components/UploadModal.js +1 -0
  9. package/src/components/__tests__/GroupActions.spec.js +23 -1
  10. package/src/components/__tests__/HistoryBackButton.spec.js +7 -0
  11. package/src/components/__tests__/TreeSelector.spec.js +8 -0
  12. package/src/components/__tests__/__snapshots__/AddMemberForm.spec.js.snap +1 -1
  13. package/src/components/__tests__/__snapshots__/AddResourceMember.spec.js.snap +1 -1
  14. package/src/components/__tests__/__snapshots__/GroupActions.spec.js.snap +9 -1
  15. package/src/components/__tests__/__snapshots__/HistoryBackButton.spec.js.snap +1 -1
  16. package/src/components/__tests__/__snapshots__/ResourceMembers.spec.js.snap +1 -1
  17. package/src/components/__tests__/__snapshots__/ResourceMembersAction.spec.js.snap +1 -1
  18. package/src/i18n/components/__tests__/__snapshots__/MessageForm.spec.js.snap +1 -1
  19. package/src/i18n/components/__tests__/__snapshots__/NewMessage.spec.js.snap +1 -1
  20. package/src/styles/IconTextControls.less +32 -0
  21. package/src/styles/OptionGroup.less +4 -0
  22. package/src/styles/TemplateSelector.less +17 -0
  23. package/src/styles/TreeSelector.less +39 -0
  24. package/src/styles/GroupActions.less +0 -3
  25. package/src/styles/graphTokens.less +0 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.5.7",
3
+ "version": "8.5.8",
4
4
  "description": "Truedat Web Core",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -54,7 +54,7 @@
54
54
  "@testing-library/jest-dom": "^6.6.3",
55
55
  "@testing-library/react": "^16.3.0",
56
56
  "@testing-library/user-event": "^14.6.1",
57
- "@truedat/test": "8.5.7",
57
+ "@truedat/test": "8.5.8",
58
58
  "identity-obj-proxy": "^3.0.0",
59
59
  "jest": "^29.7.0",
60
60
  "redux-saga-test-plan": "^4.0.6"
@@ -95,5 +95,5 @@
95
95
  "swr": "^2.3.3",
96
96
  "turndown": "^7.2.2"
97
97
  },
98
- "gitHead": "871357888d97c203cf009a04878e0277cff6af9b"
98
+ "gitHead": "61ad9443b3d822d30dcfa977f5fad3494b3ed5b4"
99
99
  }
@@ -1,18 +1,59 @@
1
+ import { useCallback, useMemo, useState } from "react";
1
2
  import PropTypes from "prop-types";
2
- import { Dropdown } from "semantic-ui-react";
3
+ import { Dropdown, Icon } from "semantic-ui-react";
4
+
5
+ const ICON_TEXT_CONTROL_CLASSNAME = "td-icon-text-control";
6
+
7
+ const normalizeAction = (action) => {
8
+ if (!action?.icon || !(action?.text || action?.content)) {
9
+ return action;
10
+ }
11
+
12
+ return {
13
+ ...action,
14
+ className: [action.className, ICON_TEXT_CONTROL_CLASSNAME]
15
+ .filter(Boolean)
16
+ .join(" "),
17
+ };
18
+ };
3
19
 
4
20
  export const GroupActions = ({
5
21
  availableActions,
6
22
  direction = "left",
7
23
  disabled,
8
24
  }) => {
25
+ const [open, setOpen] = useState(false);
26
+ const closeDropdown = useCallback(() => setOpen(false), []);
27
+
28
+ const options = useMemo(
29
+ () =>
30
+ availableActions?.map((action) => {
31
+ const { closeDropdownOnClose, ...restAction } = action;
32
+
33
+ return normalizeAction({
34
+ ...restAction,
35
+ onClick: (event, data) => {
36
+ closeDropdown();
37
+ action?.onClick?.(event, data);
38
+ },
39
+ ...(closeDropdownOnClose ? { closeDropdown } : {}),
40
+ })
41
+ }),
42
+ [availableActions, closeDropdown]
43
+ );
44
+
9
45
  return (
10
46
  <Dropdown
11
- icon="ellipsis vertical"
47
+ trigger={<Icon name="ellipsis vertical" />}
12
48
  className="button icon group-actions"
13
49
  floating
14
50
  disabled={disabled}
15
- options={availableActions}
51
+ open={open}
52
+ value={null}
53
+ onOpen={() => !disabled && setOpen(true)}
54
+ onClose={closeDropdown}
55
+ openOnFocus={false}
56
+ options={options}
16
57
  direction={direction}
17
58
  />
18
59
  );
@@ -9,6 +9,7 @@ export const HistoryBackButton = ({ content, disabled, secondary = true }) => {
9
9
  <Button
10
10
  as={Link}
11
11
  to="#"
12
+ className="td-icon-text-control"
12
13
  content={content}
13
14
  disabled={disabled}
14
15
  secondary={secondary}
@@ -47,7 +47,7 @@ const LanguagesTabs = ({
47
47
  }, [requiredLangs, altLangs]);
48
48
 
49
49
  useEffect(() => {
50
- initLang !== selectedLang && onLangChange(initLang);
50
+ initLang && initLang !== selectedLang && onLangChange(initLang);
51
51
  // eslint-disable-next-line react-hooks/exhaustive-deps
52
52
  }, [initLang]);
53
53
 
@@ -67,8 +67,11 @@ const LanguagesTabs = ({
67
67
  onLangChange(selected);
68
68
  }
69
69
  }}
70
- activeIndex={translationTabs.findIndex(
71
- (tab) => tab.lang === selectedLang
70
+ activeIndex={Math.max(
71
+ translationTabs.findIndex(
72
+ (tab) => tab.lang === (selectedLang || initLang)
73
+ ),
74
+ 0
72
75
  )}
73
76
  />
74
77
  );
@@ -15,6 +15,7 @@ export const ResourceMembersActions = ({ resource: { type, id }, actions }) => {
15
15
  <Button
16
16
  floated="right"
17
17
  primary
18
+ className="td-icon-text-control"
18
19
  content={<FormattedMessage id="domain.actions.add_member" />}
19
20
  icon="add user"
20
21
  as={Link}
@@ -30,10 +30,27 @@ export const TemplateSelector = ({
30
30
  placeholder,
31
31
  hideLabel = false,
32
32
  disabled,
33
+ liftRequiredLabel = false,
34
+ showSingleOption = false,
33
35
  }) => {
34
36
  const { formatMessage } = useIntl();
35
37
  const options = makeOptions(formatMessage)(templates);
36
- const hidden = loading ? false : _.size(templates) <= 1;
38
+ const hidden = loading
39
+ ? false
40
+ : _.isEmpty(templates) || (!showSingleOption && _.size(templates) <= 1);
41
+ const showRequiredLabel = !selectedValue && required;
42
+ const labelContent = label ? (
43
+ label
44
+ ) : (
45
+ <FormattedMessage id="template.selector.label" />
46
+ );
47
+ const shouldLiftRequiredLabel = liftRequiredLabel && showRequiredLabel;
48
+ const labelClassName = shouldLiftRequiredLabel
49
+ ? "template-selector-label template-selector-label-lifted"
50
+ : undefined;
51
+ const requiredLabelClassName = shouldLiftRequiredLabel
52
+ ? "template-selector-required-label"
53
+ : "";
37
54
 
38
55
  const handleChange = (e, { value, ...data }) => {
39
56
  const template = _.find({ id: value })(templates);
@@ -43,10 +60,10 @@ export const TemplateSelector = ({
43
60
  return hidden ? null : (
44
61
  <Form.Field required={required}>
45
62
  {hideLabel ? null : (
46
- <label>
47
- {label ? label : <FormattedMessage id="template.selector.label" />}
48
- {!selectedValue && required ? (
49
- <Label pointing="left">
63
+ <label className={labelClassName}>
64
+ {labelContent}
65
+ {showRequiredLabel ? (
66
+ <Label className={requiredLabelClassName} pointing="left">
50
67
  <FormattedMessage id="template.form.validation.empty_required" />
51
68
  </Label>
52
69
  ) : null}
@@ -88,6 +105,8 @@ TemplateSelector.propTypes = {
88
105
  placeholder: PropTypes.string,
89
106
  hideLabel: PropTypes.bool,
90
107
  disabled: PropTypes.bool,
108
+ liftRequiredLabel: PropTypes.bool,
109
+ showSingleOption: PropTypes.bool,
91
110
  };
92
111
 
93
112
  const emptyTemplates = [];
@@ -14,13 +14,18 @@ export const matchAny = recursiveMatch(match);
14
14
 
15
15
  export const SelectedLabels = ({
16
16
  disabled,
17
+ multiple,
17
18
  onClick,
18
19
  options,
19
20
  placeholder,
20
21
  value,
21
22
  }) =>
22
23
  _.isEmpty(value) ? (
23
- <label>{placeholder}</label>
24
+ <div className="default text">{placeholder}</div>
25
+ ) : !multiple && _.size(value) === 1 ? (
26
+ <div className="text">
27
+ {_.flow(_.find({ id: _.head(value) }), _.prop("name"))(options)}
28
+ </div>
24
29
  ) : (
25
30
  _.map((id) => (
26
31
  <Label key={id}>
@@ -41,6 +46,7 @@ export const SelectedLabels = ({
41
46
 
42
47
  SelectedLabels.propTypes = {
43
48
  disabled: PropTypes.bool,
49
+ multiple: PropTypes.bool,
44
50
  onClick: PropTypes.func,
45
51
  options: PropTypes.array,
46
52
  placeholder: PropTypes.string,
@@ -80,6 +86,7 @@ export const TreeSelector = ({
80
86
  }) => {
81
87
  const [query, setQuery] = useState();
82
88
  const [open, setOpen] = useState(null);
89
+ const [dropdownOpen, setDropdownOpen] = useState(false);
83
90
  const [displayed, setDisplayed] = useState([]);
84
91
  const [currentAllNodesOpen, setCurrentAllNodesOpen] = useState(false);
85
92
 
@@ -158,6 +165,9 @@ export const TreeSelector = ({
158
165
 
159
166
  const handleClick = (e, id) => {
160
167
  const nextValue = multiple ? toggle(id)(value) : value === id ? null : id;
168
+ if (!multiple) {
169
+ setDropdownOpen(false);
170
+ }
161
171
  onChange && onChange(e, { value: nextValue });
162
172
  };
163
173
 
@@ -170,6 +180,7 @@ export const TreeSelector = ({
170
180
  const trigger = labels ? (
171
181
  <SelectedLabels
172
182
  disabled={disabled}
183
+ multiple={multiple}
173
184
  placeholder={placeholder}
174
185
  options={options}
175
186
  onClick={handleClick}
@@ -238,6 +249,9 @@ export const TreeSelector = ({
238
249
  multiple={multiple}
239
250
  name={name}
240
251
  onBlur={onBlur}
252
+ onClose={() => setDropdownOpen(false)}
253
+ onOpen={() => setDropdownOpen(true)}
254
+ open={dropdownOpen}
241
255
  required={required}
242
256
  trigger={trigger}
243
257
  upward={false}
@@ -170,6 +170,7 @@ export const UploadModal = (props) => {
170
170
  </p>
171
171
  <Button
172
172
  secondary
173
+ className="td-icon-text-control"
173
174
  icon="upload"
174
175
  onClick={(e) => {
175
176
  e.stopPropagation();
@@ -1,8 +1,9 @@
1
+ import { fireEvent } from "@testing-library/react";
1
2
  import { render } from "@truedat/test/render";
2
3
  import { GroupActions } from "..";
3
4
 
4
5
  const actions = [
5
- { key: 1, text: "First Item", value: 1 },
6
+ { key: 1, icon: "edit", text: "First Item", value: 1 },
6
7
  { key: 2, text: "Second Item", value: 2 },
7
8
  ];
8
9
 
@@ -20,4 +21,25 @@ describe("<GroupActions />", () => {
20
21
  const dropdown = getByRole("listbox");
21
22
  expect(dropdown).toHaveAttribute("aria-disabled", "true");
22
23
  });
24
+
25
+ it("adds the shared class to icon and text actions", () => {
26
+ const { container } = render(<GroupActions availableActions={actions} />);
27
+ expect(container.querySelector(".item.td-icon-text-control")).toBeTruthy();
28
+ });
29
+
30
+ it("keeps the trigger icon-only after selecting an action", () => {
31
+ const { getByRole, getAllByRole } = render(
32
+ <GroupActions availableActions={actions} />
33
+ );
34
+
35
+ const dropdown = getByRole("listbox");
36
+ fireEvent.click(dropdown);
37
+ fireEvent.click(getAllByRole("option")[0]);
38
+
39
+ expect(
40
+ Array.from(dropdown.children).some((child) =>
41
+ child.classList.contains("text")
42
+ )
43
+ ).toBeFalsy();
44
+ });
23
45
  });
@@ -32,4 +32,11 @@ describe("<HistoryBackButton />", () => {
32
32
  expect(preventDefault).toHaveBeenCalled();
33
33
  expect(mockNavigate).toHaveBeenCalledWith(-1);
34
34
  });
35
+
36
+ it("uses the shared icon and text control class", async () => {
37
+ const rendered = render(<HistoryBackButton {...props} />);
38
+ await waitForLoad(rendered);
39
+
40
+ expect(rendered.getByRole("button")).toHaveClass("td-icon-text-control");
41
+ });
35
42
  });
@@ -55,6 +55,9 @@ describe("<TreeSelector />", () => {
55
55
  await user.click(rendered.getByRole("option", { name: /foo/i }));
56
56
  expect(props.onChange.mock.calls.length).toBe(1);
57
57
  expect(props.onChange.mock.calls[0][1]).toEqual({ value: ["1"] });
58
+ expect(
59
+ rendered.getByRole("option", { name: /foo/i })
60
+ ).toBeInTheDocument();
58
61
  });
59
62
 
60
63
  it("calls onChange with selected values (single)", async () => {
@@ -79,6 +82,11 @@ describe("<TreeSelector />", () => {
79
82
  await user.click(rendered.getByRole("option", { name: /foo/i }));
80
83
  expect(props.onChange.mock.calls.length).toBe(1);
81
84
  expect(props.onChange.mock.calls[0][1]).toEqual({ value: "1" });
85
+ await waitFor(() => {
86
+ expect(
87
+ rendered.getByRole("listbox")
88
+ ).toHaveAttribute("aria-expanded", "false");
89
+ });
82
90
  });
83
91
 
84
92
  it("select node with allowed depth (multiple)", async () => {
@@ -175,7 +175,7 @@ exports[`<AddMemberForm /> matches the latest snapshot 1`] = `
175
175
  domain.actions.add_member
176
176
  </button>
177
177
  <a
178
- class="ui secondary button"
178
+ class="ui secondary button td-icon-text-control"
179
179
  data-discover="true"
180
180
  href="/"
181
181
  role="button"
@@ -147,7 +147,7 @@ exports[`<AddResourceMember /> matches the latest snapshot 1`] = `
147
147
  domain.actions.add_member
148
148
  </button>
149
149
  <a
150
- class="ui secondary button"
150
+ class="ui secondary button td-icon-text-control"
151
151
  data-discover="true"
152
152
  href="/"
153
153
  role="button"
@@ -12,16 +12,24 @@ exports[`<GroupActions /> matches the latest snapshot 1`] = `
12
12
  aria-hidden="true"
13
13
  class="ellipsis vertical icon"
14
14
  />
15
+ <i
16
+ aria-hidden="true"
17
+ class="dropdown icon"
18
+ />
15
19
  <div
16
20
  class="left menu transition"
17
21
  >
18
22
  <div
19
23
  aria-checked="false"
20
24
  aria-selected="true"
21
- class="selected item"
25
+ class="selected item td-icon-text-control"
22
26
  role="option"
23
27
  style="pointer-events: all;"
24
28
  >
29
+ <i
30
+ aria-hidden="true"
31
+ class="edit icon"
32
+ />
25
33
  <span
26
34
  class="text"
27
35
  >
@@ -3,7 +3,7 @@
3
3
  exports[`<HistoryBackButton /> matches the latest snapshot 1`] = `
4
4
  <div>
5
5
  <a
6
- class="ui secondary button"
6
+ class="ui secondary button td-icon-text-control"
7
7
  data-discover="true"
8
8
  href="/"
9
9
  role="button"
@@ -22,7 +22,7 @@ exports[`<ResourceMembers /> matches the latest snapshot 1`] = `
22
22
  />
23
23
  </div>
24
24
  <a
25
- class="ui primary right floated button"
25
+ class="ui primary right floated button td-icon-text-control"
26
26
  data-discover="true"
27
27
  href="/domains/1/members/new"
28
28
  role="button"
@@ -3,7 +3,7 @@
3
3
  exports[`<ResourceMembersActions /> matches the latest snapshot 1`] = `
4
4
  <div>
5
5
  <a
6
- class="ui primary right floated button"
6
+ class="ui primary right floated button td-icon-text-control"
7
7
  data-discover="true"
8
8
  href="/domains/1/members/new"
9
9
  role="button"
@@ -35,7 +35,7 @@ exports[`<MessageForm /> matches the latest snapshot 1`] = `
35
35
  Save
36
36
  </button>
37
37
  <a
38
- class="ui secondary button"
38
+ class="ui secondary button td-icon-text-control"
39
39
  data-discover="true"
40
40
  href="/"
41
41
  role="button"
@@ -71,7 +71,7 @@ exports[`<NewMessage /> matches the latest snapshot 1`] = `
71
71
  Save
72
72
  </button>
73
73
  <a
74
- class="ui secondary button"
74
+ class="ui secondary button td-icon-text-control"
75
75
  data-discover="true"
76
76
  href="/"
77
77
  role="button"
@@ -0,0 +1,32 @@
1
+ .td-icon-text-control {
2
+ display: inline-flex !important;
3
+ align-items: center !important;
4
+ justify-content: center;
5
+ gap: 8px;
6
+ min-height: 44px;
7
+ vertical-align: middle;
8
+ line-height: 1.2;
9
+
10
+ &.fluid {
11
+ display: flex !important;
12
+ }
13
+
14
+ > .icon:not(.button):not(.dropdown),
15
+ > i.icon {
16
+ width: 16px;
17
+ height: 16px;
18
+ margin: 0;
19
+ flex: 0 0 16px;
20
+ font-size: 16px;
21
+ line-height: 1;
22
+ display: inline-flex;
23
+ align-items: center;
24
+ justify-content: center;
25
+ opacity: 1;
26
+ }
27
+
28
+ > .text,
29
+ > span {
30
+ line-height: 1.2;
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ .ui.dropdown .menu > .item.group {
2
+ padding-left: 20px !important;
3
+ padding-right: 0 !important;
4
+ }
@@ -0,0 +1,17 @@
1
+ .ui.form .field {
2
+ & > .template-selector-label.template-selector-label-lifted {
3
+ display: inline-block;
4
+ position: relative;
5
+ overflow: visible;
6
+
7
+ & > .template-selector-required-label.ui.left.pointing.label {
8
+ position: absolute;
9
+ top: 0;
10
+ left: calc(100% + 0.6666em);
11
+ margin: 0;
12
+ transform: translateY(-25%);
13
+ white-space: nowrap;
14
+ z-index: 1;
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,39 @@
1
+ .fix-dropdown-selector {
2
+ .ui.floating.dropdown {
3
+ display: inline-block;
4
+ position: relative;
5
+ cursor: pointer;
6
+ outline: 0;
7
+ width: 100%;
8
+ background: #ffffff;
9
+ border: 1px solid #d4d4d5;
10
+ border-radius: 4px;
11
+ padding: 0.5em 2.1em 0.5em 1em;
12
+ min-height: 2.71428571em;
13
+ transition: border-color 0.1s ease, box-shadow 0.1s ease;
14
+
15
+ .dropdown.icon {
16
+ position: absolute;
17
+ right: 0.75em;
18
+ top: 50%;
19
+ transform: translateY(-50%);
20
+ }
21
+
22
+ .menu.transition {
23
+ width: 100%;
24
+ }
25
+ }
26
+
27
+ .ui.floating.dropdown.visible {
28
+ z-index: 100;
29
+ }
30
+
31
+ .ui.floating.dropdown:hover,
32
+ .ui.floating.dropdown:focus-visible {
33
+ border-color: #85b7d9;
34
+ }
35
+
36
+ .ui.floating.dropdown:focus-visible {
37
+ box-shadow: 0 0 0 2px rgba(33, 133, 208, 0.18);
38
+ }
39
+ }
@@ -1,3 +0,0 @@
1
- .ui.icon.button > .icon.ellipsis.vertical{
2
- height: 1em;
3
- }
@@ -1,26 +0,0 @@
1
- :root {
2
- --td-graph-theme-primary: #ed5c17;
3
-
4
- /* Graph palette – Relations Graph (React Flow) */
5
- --td-graph-bg: #f7f8fa;
6
- --td-graph-node-bg: #ffffff;
7
- --td-graph-node-border: #d0d5e0;
8
- --td-graph-node-text: #151a24;
9
- --td-graph-node-hover-border: #2f3443;
10
- --td-graph-node-shadow: 0 2px 6px rgba(21, 26, 36, 0.08);
11
- --td-graph-node-hover-shadow: 0 6px 18px rgba(21, 26, 36, 0.14);
12
- --td-graph-node-active-bg: var(--td-graph-theme-primary);
13
- --td-graph-node-active-text: #ffffff;
14
- --td-graph-node-active-shadow: 0 4px 14px rgba(237, 92, 23, 0.35);
15
- --td-graph-edge-stroke: #a2a9b8;
16
- --td-graph-edge-label-text: #3a4150;
17
- --td-graph-edge-label-bg: #f7f8fa;
18
- --td-graph-handle-bg: #b0b8c8;
19
- --td-graph-handle-border: #ffffff;
20
- --td-graph-accent: var(--td-graph-theme-primary);
21
- --td-graph-depth-disabled: #bdbdbd;
22
- --td-graph-depth-inactive: #c9c9c9;
23
- --td-graph-border: #d4d4d5;
24
- --td-graph-border-radius: 10px;
25
-
26
- }