@gravity-ui/dashkit 5.0.0 → 5.2.0

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 (34) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/build/cjs/components/DashKit/DashKit.d.ts +3 -2
  3. package/build/cjs/components/DashKit/DashKit.js +3 -2
  4. package/build/cjs/components/OverlayControls/OverlayControls.d.ts +2 -0
  5. package/build/cjs/components/OverlayControls/OverlayControls.js +4 -3
  6. package/build/cjs/components/index.d.ts +1 -0
  7. package/build/cjs/shared/modules/__tests__/uniq-id.test.d.ts +1 -0
  8. package/build/cjs/shared/modules/__tests__/uniq-id.test.js +77 -0
  9. package/build/cjs/shared/modules/index.d.ts +1 -0
  10. package/build/cjs/shared/modules/index.js +1 -0
  11. package/build/cjs/shared/modules/uniq-id.d.ts +12 -0
  12. package/build/cjs/shared/modules/uniq-id.js +36 -0
  13. package/build/cjs/typings/config.d.ts +3 -0
  14. package/build/cjs/utils/get-new-id.d.ts +12 -0
  15. package/build/cjs/utils/get-new-id.js +9 -0
  16. package/build/cjs/utils/update-manager.d.ts +5 -3
  17. package/build/cjs/utils/update-manager.js +35 -14
  18. package/build/esm/components/DashKit/DashKit.d.ts +3 -2
  19. package/build/esm/components/DashKit/DashKit.js +3 -2
  20. package/build/esm/components/OverlayControls/OverlayControls.d.ts +2 -0
  21. package/build/esm/components/OverlayControls/OverlayControls.js +4 -3
  22. package/build/esm/components/index.d.ts +1 -0
  23. package/build/esm/shared/modules/__tests__/uniq-id.test.d.ts +1 -0
  24. package/build/esm/shared/modules/__tests__/uniq-id.test.js +74 -0
  25. package/build/esm/shared/modules/index.d.ts +1 -0
  26. package/build/esm/shared/modules/index.js +1 -0
  27. package/build/esm/shared/modules/uniq-id.d.ts +12 -0
  28. package/build/esm/shared/modules/uniq-id.js +30 -0
  29. package/build/esm/typings/config.d.ts +3 -0
  30. package/build/esm/utils/get-new-id.d.ts +12 -0
  31. package/build/esm/utils/get-new-id.js +5 -0
  32. package/build/esm/utils/update-manager.d.ts +5 -3
  33. package/build/esm/utils/update-manager.js +35 -14
  34. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [5.2.0](https://github.com/gravity-ui/dashkit/compare/v5.1.0...v5.2.0) (2023-03-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * add custom qa attribute for overlay controls ([#45](https://github.com/gravity-ui/dashkit/issues/45)) ([738ce14](https://github.com/gravity-ui/dashkit/commit/738ce14a7883a88d3096ed4c9bd21bcc16f3fd20))
9
+ * export OverlayControlItem type ([#50](https://github.com/gravity-ui/dashkit/issues/50)) ([a68cc5c](https://github.com/gravity-ui/dashkit/commit/a68cc5cb4e4470278220e18f79f9b82dc6a7cfba))
10
+
11
+ ## [5.1.0](https://github.com/gravity-ui/dashkit/compare/v5.0.0...v5.1.0) (2023-02-09)
12
+
13
+
14
+ ### Features
15
+
16
+ * generating a unique id for config ([#41](https://github.com/gravity-ui/dashkit/issues/41)) ([062c8d0](https://github.com/gravity-ui/dashkit/commit/062c8d015e334061fac193cf8c0a549a932e3a87))
17
+
3
18
  ## [5.0.0](https://github.com/gravity-ui/dashkit/compare/v4.1.0...v5.0.0) (2023-02-01)
4
19
 
5
20
 
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import GridLayout from '../GridLayout/GridLayout';
3
- import { SetConfigItem, Settings, SettingsProps, ContextProps, Plugin } from '../../typings';
3
+ import { SetConfigItem, Settings, SettingsProps, ContextProps, Plugin, SetItemOptions } from '../../typings';
4
4
  import { GlobalParams, Config, ConfigItem, ItemsStateAndParams } from '../../shared';
5
5
  import { OverlayControlItem } from '../OverlayControls/OverlayControls';
6
6
  interface DashKitGeneralProps {
@@ -29,10 +29,11 @@ export declare class DashKit extends React.PureComponent<DashKitInnerProps> {
29
29
  static defaultProps: DashKitDefaultProps;
30
30
  static registerPlugins(...plugins: Plugin[]): void;
31
31
  static setSettings(settings: Settings): void;
32
- static setItem({ item: setItem, namespace, config, }: {
32
+ static setItem({ item: setItem, namespace, config, options, }: {
33
33
  item: SetConfigItem;
34
34
  namespace?: string;
35
35
  config: Config;
36
+ options?: SetItemOptions;
36
37
  }): Config;
37
38
  static removeItem({ id, config, itemsStateAndParams, }: {
38
39
  id: string;
@@ -21,9 +21,9 @@ class DashKit extends react_1.default.PureComponent {
21
21
  static setSettings(settings) {
22
22
  registerManager.setSettings(settings);
23
23
  }
24
- static setItem({ item: setItem, namespace = constants_1.DEFAULT_NAMESPACE, config, }) {
24
+ static setItem({ item: setItem, namespace = constants_1.DEFAULT_NAMESPACE, config, options = {}, }) {
25
25
  if (setItem.id) {
26
- return utils_1.UpdateManager.editItem({ item: setItem, namespace, config });
26
+ return utils_1.UpdateManager.editItem({ item: setItem, namespace, config, options });
27
27
  }
28
28
  else {
29
29
  const item = setItem;
@@ -36,6 +36,7 @@ class DashKit extends react_1.default.PureComponent {
36
36
  namespace,
37
37
  config,
38
38
  layout,
39
+ options,
39
40
  });
40
41
  }
41
42
  }
@@ -16,6 +16,7 @@ export interface OverlayControlItem {
16
16
  allWidgetsControls?: boolean;
17
17
  excludeWidgetsTypes?: Array<PluginBase['type']>;
18
18
  id?: string;
19
+ qa?: string;
19
20
  }
20
21
  export interface OverlayCustomControlItem {
21
22
  id: string;
@@ -25,6 +26,7 @@ export interface OverlayCustomControlItem {
25
26
  handler?: (item: ConfigItem, params: StringParams) => void;
26
27
  visible?: (item: ConfigItem) => boolean;
27
28
  className?: string;
29
+ qa?: string;
28
30
  }
29
31
  interface OverlayControlsDefaultProps {
30
32
  position: OverlayControlsPosition;
@@ -26,9 +26,9 @@ class OverlayControls extends react_1.default.Component {
26
26
  super(...arguments);
27
27
  this.renderControlsItem = (item, index, length) => {
28
28
  const { view, size } = this.props;
29
- const { title, handler, icon, iconSize } = item;
29
+ const { title, handler, icon, iconSize, qa } = item;
30
30
  const onItemClickHandler = typeof handler === 'function' ? handler : noop_1.default;
31
- return (react_1.default.createElement(uikit_1.Button, { key: index, view: view, size: size, title: title, pin: this.getControlItemPinStyle(index, length), onClick: () => onItemClickHandler(this.props.configItem) },
31
+ return (react_1.default.createElement(uikit_1.Button, { key: index, view: view, size: size, title: title, pin: this.getControlItemPinStyle(index, length), onClick: () => onItemClickHandler(this.props.configItem), qa: qa },
32
32
  react_1.default.createElement(uikit_1.Icon, { data: icon || CogIcon_1.CogIcon, size: icon ? iconSize : 24 })));
33
33
  };
34
34
  this.getCustomLeftOverlayControls = () => {
@@ -171,10 +171,11 @@ class OverlayControls extends react_1.default.Component {
171
171
  icon: item.icon,
172
172
  action: itemAction,
173
173
  className: item.className,
174
+ qa: item.qa,
174
175
  };
175
176
  });
176
177
  items = items.filter(Boolean);
177
- return (react_1.default.createElement(uikit_1.DropdownMenu, { items: items, switcher: react_1.default.createElement(uikit_1.Button, { view: view, size: size, pin: "brick-round" },
178
+ return (react_1.default.createElement(uikit_1.DropdownMenu, { items: items, switcher: react_1.default.createElement(uikit_1.Button, { view: view, size: size, pin: "brick-round", qa: "dashkit-overlay-control-menu" },
178
179
  react_1.default.createElement(uikit_1.Icon, { data: DotsIcon_1.DotsIcon, size: "16" })) }));
179
180
  }
180
181
  getControlItemPinStyle(index, itemsLength) {
@@ -1 +1,2 @@
1
1
  export * from './DashKit';
2
+ export { OverlayControlItem } from './OverlayControls/OverlayControls';
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const hashids_1 = tslib_1.__importDefault(require("hashids"));
5
+ const uniq_id_1 = require("../uniq-id");
6
+ const salt = 'salt';
7
+ const hashids = new hashids_1.default(salt);
8
+ const testCounter = 1;
9
+ const id1 = hashids.encode(testCounter + 1);
10
+ const id2 = hashids.encode(testCounter + 2);
11
+ const id3 = hashids.encode(testCounter + 3);
12
+ const id4 = hashids.encode(testCounter + 4);
13
+ describe('modules.uniq-id: generateUniqId', () => {
14
+ it('return correct first id and counter', () => {
15
+ const { id, counter } = (0, uniq_id_1.generateUniqId)({
16
+ counter: testCounter,
17
+ ids: [],
18
+ salt,
19
+ });
20
+ expect(hashids.encode(counter)).toEqual(id1);
21
+ expect(id).toEqual(id1);
22
+ expect(counter).toEqual(testCounter + 1);
23
+ });
24
+ it('return correct id and counter with ids=[id1]', () => {
25
+ const { id, counter } = (0, uniq_id_1.generateUniqId)({
26
+ counter: testCounter + 1,
27
+ ids: [id1],
28
+ salt,
29
+ });
30
+ expect(hashids.encode(counter)).toEqual(id2);
31
+ expect(id).toEqual(id2);
32
+ expect(counter).toEqual(testCounter + 2);
33
+ });
34
+ it('return correct id4 and counter with ids=[id1, id2, id3] and incorrect counter=1', () => {
35
+ const { id, counter } = (0, uniq_id_1.generateUniqId)({
36
+ counter: 1,
37
+ ids: [id1, id2, id3],
38
+ salt,
39
+ });
40
+ expect(hashids.encode(counter)).toEqual(id4);
41
+ expect(id).toEqual(id4);
42
+ expect(counter).toEqual(testCounter + 4);
43
+ });
44
+ });
45
+ const IDS = ['WZ', '9X', '2m', 'R2', 'Pd'];
46
+ const config = {
47
+ items: [
48
+ {
49
+ id: IDS[0],
50
+ data: {
51
+ tabs: [
52
+ {
53
+ id: IDS[1],
54
+ },
55
+ {
56
+ id: IDS[2],
57
+ },
58
+ ],
59
+ },
60
+ },
61
+ {
62
+ id: IDS[3],
63
+ },
64
+ ],
65
+ layout: [{ i: IDS[0] }, { i: IDS[3] }],
66
+ connections: [
67
+ { from: IDS[2], to: IDS[4] },
68
+ { from: IDS[1], to: IDS[3] },
69
+ ],
70
+ };
71
+ describe('modules.uniq-id: extractIdsFromConfig', () => {
72
+ it('correct extract ids from config', () => {
73
+ const ids = (0, uniq_id_1.extractIdsFromConfig)(config);
74
+ expect(Array.isArray(ids)).toBe(true);
75
+ expect(IDS.concat().sort()).toEqual(ids.sort());
76
+ });
77
+ });
@@ -1,2 +1,3 @@
1
1
  export * from './helpers';
2
2
  export * from './state-and-params';
3
+ export * from './uniq-id';
@@ -3,3 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const tslib_1 = require("tslib");
4
4
  tslib_1.__exportStar(require("./helpers"), exports);
5
5
  tslib_1.__exportStar(require("./state-and-params"), exports);
6
+ tslib_1.__exportStar(require("./uniq-id"), exports);
@@ -0,0 +1,12 @@
1
+ import type { Config } from '../types';
2
+ export declare function extractIdsFromConfig(config: Config): string[];
3
+ type GenerateUniqIdArgs = {
4
+ salt: Config['salt'];
5
+ counter: Config['counter'];
6
+ ids: string[];
7
+ };
8
+ export declare function generateUniqId({ salt, counter, ids }: GenerateUniqIdArgs): {
9
+ counter: number;
10
+ id: string;
11
+ };
12
+ export {};
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateUniqId = exports.extractIdsFromConfig = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const hashids_1 = tslib_1.__importDefault(require("hashids"));
6
+ const helpers_1 = require("./helpers");
7
+ function extractIdsFromConfig(config) {
8
+ const ids = [];
9
+ const items = config.items || [];
10
+ const connections = config.connections || [];
11
+ const layout = config.layout || [];
12
+ items.forEach((item) => {
13
+ ids.push(item.id);
14
+ if ((0, helpers_1.isItemWithTabs)(item)) {
15
+ item.data.tabs.forEach((tabItem) => ids.push(tabItem.id));
16
+ }
17
+ });
18
+ connections.forEach(({ from, to }) => ids.push(from, to));
19
+ layout.forEach(({ i }) => ids.push(i));
20
+ return Array.from(new Set(ids));
21
+ }
22
+ exports.extractIdsFromConfig = extractIdsFromConfig;
23
+ function generateUniqId({ salt, counter, ids }) {
24
+ let newCounter = counter;
25
+ let uniqId = null;
26
+ const idsSet = new Set(ids);
27
+ const hashids = new hashids_1.default(salt);
28
+ while (uniqId === null) {
29
+ const id = hashids.encode(++newCounter);
30
+ if (!idsSet.has(id)) {
31
+ uniqId = id;
32
+ }
33
+ }
34
+ return { counter: newCounter, id: uniqId };
35
+ }
36
+ exports.generateUniqId = generateUniqId;
@@ -5,3 +5,6 @@ export interface AddConfigItem extends Omit<ConfigItem, 'id' | 'namespace'> {
5
5
  layout?: ConfigLayout;
6
6
  }
7
7
  export type SetConfigItem = ConfigItem | AddConfigItem;
8
+ export type SetItemOptions = {
9
+ excludeIds?: string[];
10
+ };
@@ -0,0 +1,12 @@
1
+ import { Config } from '../shared';
2
+ type GetNewIdArgs = {
3
+ config: Config;
4
+ salt: Config['salt'];
5
+ counter: Config['counter'];
6
+ excludeIds?: string[];
7
+ };
8
+ export declare function getNewId({ config, salt, counter, excludeIds }: GetNewIdArgs): {
9
+ counter: number;
10
+ id: string;
11
+ };
12
+ export {};
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getNewId = void 0;
4
+ const shared_1 = require("../shared");
5
+ function getNewId({ config, salt, counter, excludeIds = [] }) {
6
+ const allIds = [...(0, shared_1.extractIdsFromConfig)(config), ...excludeIds];
7
+ return (0, shared_1.generateUniqId)({ salt, counter, ids: allIds });
8
+ }
9
+ exports.getNewId = getNewId;
@@ -1,5 +1,5 @@
1
1
  import { Config, ConfigItem, ItemsStateAndParams, ItemStateAndParams } from '../shared';
2
- import { AddConfigItem, WidgetLayout } from '../typings';
2
+ import { AddConfigItem, WidgetLayout, SetItemOptions } from '../typings';
3
3
  import { RegisterManagerPluginLayout } from './register-manager';
4
4
  interface RemoveItemArg {
5
5
  id: string;
@@ -13,16 +13,18 @@ interface ChangeStateAndParamsArg {
13
13
  stateAndParams: ItemStateAndParams;
14
14
  }
15
15
  export declare class UpdateManager {
16
- static addItem({ item, namespace, layout, config, }: {
16
+ static addItem({ item, namespace, layout, config, options, }: {
17
17
  item: AddConfigItem;
18
18
  namespace: string;
19
19
  layout: RegisterManagerPluginLayout;
20
20
  config: Config;
21
+ options: SetItemOptions;
21
22
  }): Config;
22
- static editItem({ item, namespace, config, }: {
23
+ static editItem({ item, namespace, config, options, }: {
23
24
  item: ConfigItem;
24
25
  namespace: string;
25
26
  config: Config;
27
+ options: SetItemOptions;
26
28
  }): Config;
27
29
  static removeItem({ id, config, itemsStateAndParams }: RemoveItemArg): {
28
30
  config: Config;
@@ -3,11 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.UpdateManager = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const immutability_helper_1 = tslib_1.__importStar(require("immutability-helper"));
6
- const hashids_1 = tslib_1.__importDefault(require("hashids"));
7
6
  const omit_1 = tslib_1.__importDefault(require("lodash/omit"));
8
7
  const pick_1 = tslib_1.__importDefault(require("lodash/pick"));
9
8
  const shared_1 = require("../shared");
10
9
  const constants_1 = require("../constants");
10
+ const get_new_id_1 = require("./get-new-id");
11
11
  (0, immutability_helper_1.extend)('$auto', (value, object) => (object ? (0, immutability_helper_1.default)(object, value) : (0, immutability_helper_1.default)({}, value)));
12
12
  function removeItemVersion1({ id, config, itemsStateAndParams }) {
13
13
  const itemIndex = config.items.findIndex((item) => item.id === id);
@@ -134,30 +134,51 @@ function changeStateAndParamsVersion1({ id: initiatorId, config, stateAndParams,
134
134
  return itemsStateAndParams;
135
135
  }
136
136
  }
137
+ function getNewItemData({ item, config, counter: argsCounter, salt, options }) {
138
+ const excludeIds = [...(options.excludeIds || [])];
139
+ let counter = argsCounter;
140
+ let data = item.data;
141
+ if ((0, shared_1.isItemWithTabs)(item)) {
142
+ const tabs = item.data.tabs.map((tab) => {
143
+ if (tab.id) {
144
+ return tab;
145
+ }
146
+ const newIdTabData = (0, get_new_id_1.getNewId)({ config, salt, counter, excludeIds });
147
+ counter = newIdTabData.counter;
148
+ excludeIds.push(newIdTabData.id);
149
+ return Object.assign(Object.assign({}, tab), { id: newIdTabData.id });
150
+ });
151
+ data = Object.assign(Object.assign({}, item.data), { tabs });
152
+ }
153
+ return { data, counter, excludeIds };
154
+ }
137
155
  class UpdateManager {
138
- // TODO: проверять, что id нового элемента уникальный
139
- static addItem({ item, namespace = constants_1.DEFAULT_NAMESPACE, layout, config, }) {
156
+ static addItem({ item, namespace = constants_1.DEFAULT_NAMESPACE, layout, config, options, }) {
140
157
  const layoutY = Math.max(0, ...config.layout.map(({ h, y }) => h + y));
141
158
  const saveDefaultLayout = (0, pick_1.default)(layout, ['h', 'w', 'x', 'y']);
142
- const hashids = new hashids_1.default(config.salt);
143
- let counter = config.counter;
144
- const resultData = (0, shared_1.isItemWithTabs)(item)
145
- ? Object.assign(Object.assign({}, item.data), { tabs: item.data.tabs.map((tab) => tab.id ? tab : Object.assign(Object.assign({}, tab), { id: hashids.encode(++counter) })) }) : item.data;
146
- const newItem = Object.assign(Object.assign({}, item), { id: hashids.encode(++counter), data: resultData, namespace });
159
+ const salt = config.salt;
160
+ const newItemData = getNewItemData({ item, config, salt, counter: config.counter, options });
161
+ let counter = newItemData.counter;
162
+ const newIdData = (0, get_new_id_1.getNewId)({ config, salt, counter, excludeIds: newItemData.excludeIds });
163
+ counter = newIdData.counter;
164
+ const newItem = Object.assign(Object.assign({}, item), { id: newIdData.id, data: newItemData.data, namespace });
147
165
  return (0, immutability_helper_1.default)(config, {
148
166
  items: { $push: [newItem] },
149
167
  layout: { $push: [Object.assign(Object.assign({}, saveDefaultLayout), { y: layoutY, i: newItem.id })] },
150
168
  counter: { $set: counter },
151
169
  });
152
170
  }
153
- static editItem({ item, namespace = constants_1.DEFAULT_NAMESPACE, config, }) {
154
- const hashids = new hashids_1.default(config.salt);
155
- let counter = config.counter;
171
+ static editItem({ item, namespace = constants_1.DEFAULT_NAMESPACE, config, options, }) {
156
172
  const itemIndex = config.items.findIndex(({ id }) => item.id === id);
157
- const resultData = (0, shared_1.isItemWithTabs)(item)
158
- ? Object.assign(Object.assign({}, item.data), { tabs: item.data.tabs.map((tab) => tab.id ? tab : Object.assign(Object.assign({}, tab), { id: hashids.encode(++counter) })) }) : item.data;
173
+ const { counter, data } = getNewItemData({
174
+ item,
175
+ config,
176
+ salt: config.salt,
177
+ counter: config.counter,
178
+ options,
179
+ });
159
180
  return (0, immutability_helper_1.default)(config, {
160
- items: { [itemIndex]: { $set: Object.assign(Object.assign({}, item), { data: resultData, namespace }) } },
181
+ items: { [itemIndex]: { $set: Object.assign(Object.assign({}, item), { data, namespace }) } },
161
182
  counter: { $set: counter },
162
183
  });
163
184
  }
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import GridLayout from '../GridLayout/GridLayout';
3
- import { SetConfigItem, Settings, SettingsProps, ContextProps, Plugin } from '../../typings';
3
+ import { SetConfigItem, Settings, SettingsProps, ContextProps, Plugin, SetItemOptions } from '../../typings';
4
4
  import { GlobalParams, Config, ConfigItem, ItemsStateAndParams } from '../../shared';
5
5
  import { OverlayControlItem } from '../OverlayControls/OverlayControls';
6
6
  interface DashKitGeneralProps {
@@ -29,10 +29,11 @@ export declare class DashKit extends React.PureComponent<DashKitInnerProps> {
29
29
  static defaultProps: DashKitDefaultProps;
30
30
  static registerPlugins(...plugins: Plugin[]): void;
31
31
  static setSettings(settings: Settings): void;
32
- static setItem({ item: setItem, namespace, config, }: {
32
+ static setItem({ item: setItem, namespace, config, options, }: {
33
33
  item: SetConfigItem;
34
34
  namespace?: string;
35
35
  config: Config;
36
+ options?: SetItemOptions;
36
37
  }): Config;
37
38
  static removeItem({ id, config, itemsStateAndParams, }: {
38
39
  id: string;
@@ -17,9 +17,9 @@ export class DashKit extends React.PureComponent {
17
17
  static setSettings(settings) {
18
18
  registerManager.setSettings(settings);
19
19
  }
20
- static setItem({ item: setItem, namespace = DEFAULT_NAMESPACE, config, }) {
20
+ static setItem({ item: setItem, namespace = DEFAULT_NAMESPACE, config, options = {}, }) {
21
21
  if (setItem.id) {
22
- return UpdateManager.editItem({ item: setItem, namespace, config });
22
+ return UpdateManager.editItem({ item: setItem, namespace, config, options });
23
23
  }
24
24
  else {
25
25
  const item = setItem;
@@ -32,6 +32,7 @@ export class DashKit extends React.PureComponent {
32
32
  namespace,
33
33
  config,
34
34
  layout,
35
+ options,
35
36
  });
36
37
  }
37
38
  }
@@ -17,6 +17,7 @@ export interface OverlayControlItem {
17
17
  allWidgetsControls?: boolean;
18
18
  excludeWidgetsTypes?: Array<PluginBase['type']>;
19
19
  id?: string;
20
+ qa?: string;
20
21
  }
21
22
  export interface OverlayCustomControlItem {
22
23
  id: string;
@@ -26,6 +27,7 @@ export interface OverlayCustomControlItem {
26
27
  handler?: (item: ConfigItem, params: StringParams) => void;
27
28
  visible?: (item: ConfigItem) => boolean;
28
29
  className?: string;
30
+ qa?: string;
29
31
  }
30
32
  interface OverlayControlsDefaultProps {
31
33
  position: OverlayControlsPosition;
@@ -23,9 +23,9 @@ class OverlayControls extends React.Component {
23
23
  super(...arguments);
24
24
  this.renderControlsItem = (item, index, length) => {
25
25
  const { view, size } = this.props;
26
- const { title, handler, icon, iconSize } = item;
26
+ const { title, handler, icon, iconSize, qa } = item;
27
27
  const onItemClickHandler = typeof handler === 'function' ? handler : noop;
28
- return (React.createElement(Button, { key: index, view: view, size: size, title: title, pin: this.getControlItemPinStyle(index, length), onClick: () => onItemClickHandler(this.props.configItem) },
28
+ return (React.createElement(Button, { key: index, view: view, size: size, title: title, pin: this.getControlItemPinStyle(index, length), onClick: () => onItemClickHandler(this.props.configItem), qa: qa },
29
29
  React.createElement(Icon, { data: icon || CogIcon, size: icon ? iconSize : 24 })));
30
30
  };
31
31
  this.getCustomLeftOverlayControls = () => {
@@ -168,10 +168,11 @@ class OverlayControls extends React.Component {
168
168
  icon: item.icon,
169
169
  action: itemAction,
170
170
  className: item.className,
171
+ qa: item.qa,
171
172
  };
172
173
  });
173
174
  items = items.filter(Boolean);
174
- return (React.createElement(DropdownMenu, { items: items, switcher: React.createElement(Button, { view: view, size: size, pin: "brick-round" },
175
+ return (React.createElement(DropdownMenu, { items: items, switcher: React.createElement(Button, { view: view, size: size, pin: "brick-round", qa: "dashkit-overlay-control-menu" },
175
176
  React.createElement(Icon, { data: DotsIcon, size: "16" })) }));
176
177
  }
177
178
  getControlItemPinStyle(index, itemsLength) {
@@ -1 +1,2 @@
1
1
  export * from './DashKit';
2
+ export { OverlayControlItem } from './OverlayControls/OverlayControls';
@@ -0,0 +1,74 @@
1
+ import Hashids from 'hashids';
2
+ import { generateUniqId, extractIdsFromConfig } from '../uniq-id';
3
+ const salt = 'salt';
4
+ const hashids = new Hashids(salt);
5
+ const testCounter = 1;
6
+ const id1 = hashids.encode(testCounter + 1);
7
+ const id2 = hashids.encode(testCounter + 2);
8
+ const id3 = hashids.encode(testCounter + 3);
9
+ const id4 = hashids.encode(testCounter + 4);
10
+ describe('modules.uniq-id: generateUniqId', () => {
11
+ it('return correct first id and counter', () => {
12
+ const { id, counter } = generateUniqId({
13
+ counter: testCounter,
14
+ ids: [],
15
+ salt,
16
+ });
17
+ expect(hashids.encode(counter)).toEqual(id1);
18
+ expect(id).toEqual(id1);
19
+ expect(counter).toEqual(testCounter + 1);
20
+ });
21
+ it('return correct id and counter with ids=[id1]', () => {
22
+ const { id, counter } = generateUniqId({
23
+ counter: testCounter + 1,
24
+ ids: [id1],
25
+ salt,
26
+ });
27
+ expect(hashids.encode(counter)).toEqual(id2);
28
+ expect(id).toEqual(id2);
29
+ expect(counter).toEqual(testCounter + 2);
30
+ });
31
+ it('return correct id4 and counter with ids=[id1, id2, id3] and incorrect counter=1', () => {
32
+ const { id, counter } = generateUniqId({
33
+ counter: 1,
34
+ ids: [id1, id2, id3],
35
+ salt,
36
+ });
37
+ expect(hashids.encode(counter)).toEqual(id4);
38
+ expect(id).toEqual(id4);
39
+ expect(counter).toEqual(testCounter + 4);
40
+ });
41
+ });
42
+ const IDS = ['WZ', '9X', '2m', 'R2', 'Pd'];
43
+ const config = {
44
+ items: [
45
+ {
46
+ id: IDS[0],
47
+ data: {
48
+ tabs: [
49
+ {
50
+ id: IDS[1],
51
+ },
52
+ {
53
+ id: IDS[2],
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ {
59
+ id: IDS[3],
60
+ },
61
+ ],
62
+ layout: [{ i: IDS[0] }, { i: IDS[3] }],
63
+ connections: [
64
+ { from: IDS[2], to: IDS[4] },
65
+ { from: IDS[1], to: IDS[3] },
66
+ ],
67
+ };
68
+ describe('modules.uniq-id: extractIdsFromConfig', () => {
69
+ it('correct extract ids from config', () => {
70
+ const ids = extractIdsFromConfig(config);
71
+ expect(Array.isArray(ids)).toBe(true);
72
+ expect(IDS.concat().sort()).toEqual(ids.sort());
73
+ });
74
+ });
@@ -1,2 +1,3 @@
1
1
  export * from './helpers';
2
2
  export * from './state-and-params';
3
+ export * from './uniq-id';
@@ -1,2 +1,3 @@
1
1
  export * from './helpers';
2
2
  export * from './state-and-params';
3
+ export * from './uniq-id';
@@ -0,0 +1,12 @@
1
+ import type { Config } from '../types';
2
+ export declare function extractIdsFromConfig(config: Config): string[];
3
+ type GenerateUniqIdArgs = {
4
+ salt: Config['salt'];
5
+ counter: Config['counter'];
6
+ ids: string[];
7
+ };
8
+ export declare function generateUniqId({ salt, counter, ids }: GenerateUniqIdArgs): {
9
+ counter: number;
10
+ id: string;
11
+ };
12
+ export {};
@@ -0,0 +1,30 @@
1
+ import Hashids from 'hashids';
2
+ import { isItemWithTabs } from './helpers';
3
+ export function extractIdsFromConfig(config) {
4
+ const ids = [];
5
+ const items = config.items || [];
6
+ const connections = config.connections || [];
7
+ const layout = config.layout || [];
8
+ items.forEach((item) => {
9
+ ids.push(item.id);
10
+ if (isItemWithTabs(item)) {
11
+ item.data.tabs.forEach((tabItem) => ids.push(tabItem.id));
12
+ }
13
+ });
14
+ connections.forEach(({ from, to }) => ids.push(from, to));
15
+ layout.forEach(({ i }) => ids.push(i));
16
+ return Array.from(new Set(ids));
17
+ }
18
+ export function generateUniqId({ salt, counter, ids }) {
19
+ let newCounter = counter;
20
+ let uniqId = null;
21
+ const idsSet = new Set(ids);
22
+ const hashids = new Hashids(salt);
23
+ while (uniqId === null) {
24
+ const id = hashids.encode(++newCounter);
25
+ if (!idsSet.has(id)) {
26
+ uniqId = id;
27
+ }
28
+ }
29
+ return { counter: newCounter, id: uniqId };
30
+ }
@@ -5,3 +5,6 @@ export interface AddConfigItem extends Omit<ConfigItem, 'id' | 'namespace'> {
5
5
  layout?: ConfigLayout;
6
6
  }
7
7
  export type SetConfigItem = ConfigItem | AddConfigItem;
8
+ export type SetItemOptions = {
9
+ excludeIds?: string[];
10
+ };
@@ -0,0 +1,12 @@
1
+ import { Config } from '../shared';
2
+ type GetNewIdArgs = {
3
+ config: Config;
4
+ salt: Config['salt'];
5
+ counter: Config['counter'];
6
+ excludeIds?: string[];
7
+ };
8
+ export declare function getNewId({ config, salt, counter, excludeIds }: GetNewIdArgs): {
9
+ counter: number;
10
+ id: string;
11
+ };
12
+ export {};
@@ -0,0 +1,5 @@
1
+ import { extractIdsFromConfig, generateUniqId } from '../shared';
2
+ export function getNewId({ config, salt, counter, excludeIds = [] }) {
3
+ const allIds = [...extractIdsFromConfig(config), ...excludeIds];
4
+ return generateUniqId({ salt, counter, ids: allIds });
5
+ }
@@ -1,5 +1,5 @@
1
1
  import { Config, ConfigItem, ItemsStateAndParams, ItemStateAndParams } from '../shared';
2
- import { AddConfigItem, WidgetLayout } from '../typings';
2
+ import { AddConfigItem, WidgetLayout, SetItemOptions } from '../typings';
3
3
  import { RegisterManagerPluginLayout } from './register-manager';
4
4
  interface RemoveItemArg {
5
5
  id: string;
@@ -13,16 +13,18 @@ interface ChangeStateAndParamsArg {
13
13
  stateAndParams: ItemStateAndParams;
14
14
  }
15
15
  export declare class UpdateManager {
16
- static addItem({ item, namespace, layout, config, }: {
16
+ static addItem({ item, namespace, layout, config, options, }: {
17
17
  item: AddConfigItem;
18
18
  namespace: string;
19
19
  layout: RegisterManagerPluginLayout;
20
20
  config: Config;
21
+ options: SetItemOptions;
21
22
  }): Config;
22
- static editItem({ item, namespace, config, }: {
23
+ static editItem({ item, namespace, config, options, }: {
23
24
  item: ConfigItem;
24
25
  namespace: string;
25
26
  config: Config;
27
+ options: SetItemOptions;
26
28
  }): Config;
27
29
  static removeItem({ id, config, itemsStateAndParams }: RemoveItemArg): {
28
30
  config: Config;
@@ -1,9 +1,9 @@
1
1
  import update, { extend } from 'immutability-helper';
2
- import Hashids from 'hashids';
3
2
  import omit from 'lodash/omit';
4
3
  import pick from 'lodash/pick';
5
4
  import { mergeParamsWithAliases, isItemWithTabs, getCurrentVersion, META_KEY, deleteFromQueue, addToQueue, getInitialItemsStateAndParamsMeta, resolveItemInnerId, getItemsStateAndParamsMeta, } from '../shared';
6
5
  import { DEFAULT_NAMESPACE } from '../constants';
6
+ import { getNewId } from './get-new-id';
7
7
  extend('$auto', (value, object) => (object ? update(object, value) : update({}, value)));
8
8
  function removeItemVersion1({ id, config, itemsStateAndParams }) {
9
9
  const itemIndex = config.items.findIndex((item) => item.id === id);
@@ -130,30 +130,51 @@ function changeStateAndParamsVersion1({ id: initiatorId, config, stateAndParams,
130
130
  return itemsStateAndParams;
131
131
  }
132
132
  }
133
+ function getNewItemData({ item, config, counter: argsCounter, salt, options }) {
134
+ const excludeIds = [...(options.excludeIds || [])];
135
+ let counter = argsCounter;
136
+ let data = item.data;
137
+ if (isItemWithTabs(item)) {
138
+ const tabs = item.data.tabs.map((tab) => {
139
+ if (tab.id) {
140
+ return tab;
141
+ }
142
+ const newIdTabData = getNewId({ config, salt, counter, excludeIds });
143
+ counter = newIdTabData.counter;
144
+ excludeIds.push(newIdTabData.id);
145
+ return Object.assign(Object.assign({}, tab), { id: newIdTabData.id });
146
+ });
147
+ data = Object.assign(Object.assign({}, item.data), { tabs });
148
+ }
149
+ return { data, counter, excludeIds };
150
+ }
133
151
  export class UpdateManager {
134
- // TODO: проверять, что id нового элемента уникальный
135
- static addItem({ item, namespace = DEFAULT_NAMESPACE, layout, config, }) {
152
+ static addItem({ item, namespace = DEFAULT_NAMESPACE, layout, config, options, }) {
136
153
  const layoutY = Math.max(0, ...config.layout.map(({ h, y }) => h + y));
137
154
  const saveDefaultLayout = pick(layout, ['h', 'w', 'x', 'y']);
138
- const hashids = new Hashids(config.salt);
139
- let counter = config.counter;
140
- const resultData = isItemWithTabs(item)
141
- ? Object.assign(Object.assign({}, item.data), { tabs: item.data.tabs.map((tab) => tab.id ? tab : Object.assign(Object.assign({}, tab), { id: hashids.encode(++counter) })) }) : item.data;
142
- const newItem = Object.assign(Object.assign({}, item), { id: hashids.encode(++counter), data: resultData, namespace });
155
+ const salt = config.salt;
156
+ const newItemData = getNewItemData({ item, config, salt, counter: config.counter, options });
157
+ let counter = newItemData.counter;
158
+ const newIdData = getNewId({ config, salt, counter, excludeIds: newItemData.excludeIds });
159
+ counter = newIdData.counter;
160
+ const newItem = Object.assign(Object.assign({}, item), { id: newIdData.id, data: newItemData.data, namespace });
143
161
  return update(config, {
144
162
  items: { $push: [newItem] },
145
163
  layout: { $push: [Object.assign(Object.assign({}, saveDefaultLayout), { y: layoutY, i: newItem.id })] },
146
164
  counter: { $set: counter },
147
165
  });
148
166
  }
149
- static editItem({ item, namespace = DEFAULT_NAMESPACE, config, }) {
150
- const hashids = new Hashids(config.salt);
151
- let counter = config.counter;
167
+ static editItem({ item, namespace = DEFAULT_NAMESPACE, config, options, }) {
152
168
  const itemIndex = config.items.findIndex(({ id }) => item.id === id);
153
- const resultData = isItemWithTabs(item)
154
- ? Object.assign(Object.assign({}, item.data), { tabs: item.data.tabs.map((tab) => tab.id ? tab : Object.assign(Object.assign({}, tab), { id: hashids.encode(++counter) })) }) : item.data;
169
+ const { counter, data } = getNewItemData({
170
+ item,
171
+ config,
172
+ salt: config.salt,
173
+ counter: config.counter,
174
+ options,
175
+ });
155
176
  return update(config, {
156
- items: { [itemIndex]: { $set: Object.assign(Object.assign({}, item), { data: resultData, namespace }) } },
177
+ items: { [itemIndex]: { $set: Object.assign(Object.assign({}, item), { data, namespace }) } },
157
178
  counter: { $set: counter },
158
179
  });
159
180
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/dashkit",
3
- "version": "5.0.0",
3
+ "version": "5.2.0",
4
4
  "description": "Library for rendering dashboard grid layout",
5
5
  "license": "MIT",
6
6
  "repository": {