@teambit/component.ui.version-dropdown 0.0.0-0aa660598a05d04f2b124cb0ae8b4a1c05b66d78

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 (52) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.js +9 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/lane-info/index.d.ts +2 -0
  5. package/dist/lane-info/index.js +6 -0
  6. package/dist/lane-info/index.js.map +1 -0
  7. package/dist/lane-info/lane-info.d.ts +6 -0
  8. package/dist/lane-info/lane-info.js +20 -0
  9. package/dist/lane-info/lane-info.js.map +1 -0
  10. package/dist/lane-info/lane-info.module.scss +27 -0
  11. package/dist/preview-1753890535938.js +7 -0
  12. package/dist/version-dropdown-placeholder.d.ts +17 -0
  13. package/dist/version-dropdown-placeholder.js +92 -0
  14. package/dist/version-dropdown-placeholder.js.map +1 -0
  15. package/dist/version-dropdown-placeholder.module.scss +50 -0
  16. package/dist/version-dropdown.composition.d.ts +3 -0
  17. package/dist/version-dropdown.composition.js +28 -0
  18. package/dist/version-dropdown.composition.js.map +1 -0
  19. package/dist/version-dropdown.d.ts +53 -0
  20. package/dist/version-dropdown.docs.d.ts +35 -0
  21. package/dist/version-dropdown.docs.js +67 -0
  22. package/dist/version-dropdown.docs.js.map +1 -0
  23. package/dist/version-dropdown.js +153 -0
  24. package/dist/version-dropdown.js.map +1 -0
  25. package/dist/version-dropdown.module.scss +128 -0
  26. package/dist/version-dropdown.spec.d.ts +1 -0
  27. package/dist/version-dropdown.spec.js +33 -0
  28. package/dist/version-dropdown.spec.js.map +1 -0
  29. package/dist/version-info/index.d.ts +2 -0
  30. package/dist/version-info/index.js +6 -0
  31. package/dist/version-info/index.js.map +1 -0
  32. package/dist/version-info/version-info.d.ts +10 -0
  33. package/dist/version-info/version-info.js +79 -0
  34. package/dist/version-info/version-info.js.map +1 -0
  35. package/dist/version-info/version-info.module.scss +54 -0
  36. package/index.ts +4 -0
  37. package/lane-info/index.ts +2 -0
  38. package/lane-info/lane-info.module.scss +27 -0
  39. package/lane-info/lane-info.tsx +23 -0
  40. package/package.json +71 -0
  41. package/types/asset.d.ts +29 -0
  42. package/types/style.d.ts +42 -0
  43. package/version-dropdown-placeholder.module.scss +50 -0
  44. package/version-dropdown-placeholder.tsx +109 -0
  45. package/version-dropdown.composition.tsx +36 -0
  46. package/version-dropdown.docs.tsx +68 -0
  47. package/version-dropdown.module.scss +128 -0
  48. package/version-dropdown.spec.tsx +29 -0
  49. package/version-dropdown.tsx +331 -0
  50. package/version-info/index.ts +2 -0
  51. package/version-info/version-info.module.scss +54 -0
  52. package/version-info/version-info.tsx +87 -0
@@ -0,0 +1,331 @@
1
+ import React, { useState } from 'react';
2
+ import { MenuLinkItem } from '@teambit/design.ui.surfaces.menu.link-item';
3
+ import { Dropdown } from '@teambit/evangelist.surfaces.dropdown';
4
+ import { Tab } from '@teambit/ui-foundation.ui.use-box.tab';
5
+ import type { LegacyComponentLog } from '@teambit/legacy-component-log';
6
+ import { UserAvatar } from '@teambit/design.ui.avatar';
7
+ import { LineSkeleton } from '@teambit/base-ui.loaders.skeleton';
8
+ import type { LaneModel } from '@teambit/lanes.ui.models.lanes-model';
9
+ import classNames from 'classnames';
10
+ import styles from './version-dropdown.module.scss';
11
+ import { VersionInfo } from './version-info';
12
+ import { LaneInfo } from './lane-info';
13
+ import type { VersionProps } from './version-dropdown-placeholder';
14
+ import { SimpleVersion } from './version-dropdown-placeholder';
15
+
16
+ export const LOCAL_VERSION = 'workspace';
17
+
18
+ export type DropdownComponentVersion = Partial<LegacyComponentLog> & { version: string };
19
+
20
+ export type UseComponentDropdownVersionsResult = {
21
+ tags?: DropdownComponentVersion[];
22
+ snaps?: DropdownComponentVersion[];
23
+ loading?: boolean;
24
+ };
25
+ export type UseComponentDropdownVersionsProps = {
26
+ skip?: boolean;
27
+ };
28
+ export type UseComponentDropdownVersions = (
29
+ props?: UseComponentDropdownVersionsProps
30
+ ) => UseComponentDropdownVersionsResult;
31
+ export type GetActiveTabIndex = (
32
+ currentVersion?: string,
33
+ tabs?: Array<VersionMenuTab>,
34
+ tags?: DropdownComponentVersion[],
35
+ snaps?: DropdownComponentVersion[],
36
+ currentLane?: LaneModel
37
+ ) => number;
38
+ export type VersionDropdownProps = {
39
+ localVersion?: boolean;
40
+ latestVersion?: string;
41
+ currentVersion: string;
42
+ useCurrentVersionLog?: (props?: { skip?: boolean; version?: string }) => DropdownComponentVersion | undefined;
43
+ hasMoreVersions?: boolean;
44
+ loading?: boolean;
45
+ useComponentVersions?: UseComponentDropdownVersions;
46
+ currentLane?: LaneModel;
47
+ lanes?: LaneModel[];
48
+ getActiveTabIndex?: GetActiveTabIndex;
49
+ overrideVersionHref?: (version: string) => string;
50
+ placeholderClassName?: string;
51
+ dropdownClassName?: string;
52
+ menuClassName?: string;
53
+ showVersionDetails?: boolean;
54
+ disabled?: boolean;
55
+ PlaceholderComponent?: React.ComponentType<VersionProps>;
56
+ } & React.HTMLAttributes<HTMLDivElement>;
57
+
58
+ export const VersionDropdown = React.memo(_VersionDropdown);
59
+ const VersionMenu = React.memo(_VersionMenu);
60
+ function _VersionDropdown({
61
+ currentVersion,
62
+ latestVersion,
63
+ localVersion,
64
+ useCurrentVersionLog,
65
+ hasMoreVersions,
66
+ loading,
67
+ overrideVersionHref,
68
+ className,
69
+ placeholderClassName,
70
+ getActiveTabIndex,
71
+ dropdownClassName,
72
+ menuClassName,
73
+ showVersionDetails = true,
74
+ disabled,
75
+ PlaceholderComponent: _PlaceholderComponent,
76
+ currentLane,
77
+ useComponentVersions,
78
+ lanes,
79
+ ...rest
80
+ }: VersionDropdownProps) {
81
+ const [key, setKey] = useState(0);
82
+ const singleVersion = !hasMoreVersions;
83
+ const [open, setOpen] = useState(false);
84
+
85
+ React.useEffect(() => {
86
+ if (loading && open) {
87
+ setOpen(false);
88
+ }
89
+ }, [loading]);
90
+
91
+ const handlePlaceholderClicked = (e: React.MouseEvent<HTMLDivElement>) => {
92
+ if (loading) return;
93
+ if (e.target === e.currentTarget) {
94
+ setOpen((o) => !o);
95
+ }
96
+ };
97
+
98
+ const defaultPlaceholder = (
99
+ <SimpleVersion
100
+ useCurrentVersionLog={useCurrentVersionLog}
101
+ disabled={disabled}
102
+ className={placeholderClassName}
103
+ currentVersion={currentVersion}
104
+ onClick={handlePlaceholderClicked}
105
+ hasMoreVersions={hasMoreVersions}
106
+ loading={loading}
107
+ showFullVersion={currentVersion === 'workspace'}
108
+ />
109
+ );
110
+
111
+ const PlaceholderComponent = _PlaceholderComponent ? (
112
+ <_PlaceholderComponent
113
+ useCurrentVersionLog={useCurrentVersionLog}
114
+ disabled={disabled}
115
+ className={placeholderClassName}
116
+ currentVersion={currentVersion}
117
+ onClick={handlePlaceholderClicked}
118
+ hasMoreVersions={hasMoreVersions}
119
+ loading={loading}
120
+ showFullVersion={currentVersion === 'workspace'}
121
+ />
122
+ ) : (
123
+ defaultPlaceholder
124
+ );
125
+
126
+ if (disabled || (singleVersion && !loading)) {
127
+ return <div className={classNames(styles.noVersions, className)}>{PlaceholderComponent}</div>;
128
+ }
129
+
130
+ return (
131
+ <div {...rest} className={classNames(styles.versionDropdown, className)}>
132
+ <Dropdown
133
+ className={classNames(styles.dropdown, dropdownClassName)}
134
+ dropClass={classNames(styles.menu, menuClassName)}
135
+ open={open}
136
+ onClick={handlePlaceholderClicked}
137
+ onClickOutside={() => setOpen(false)}
138
+ onChange={(_e, _open) => _open && setKey((x) => x + 1)} // to reset menu to initial state when toggling
139
+ PlaceholderComponent={({ children, ...other }) => (
140
+ <div {...other} className={placeholderClassName} onClick={handlePlaceholderClicked}>
141
+ {children}
142
+ </div>
143
+ )}
144
+ placeholder={PlaceholderComponent}
145
+ >
146
+ <VersionMenu
147
+ className={menuClassName}
148
+ key={key}
149
+ currentVersion={currentVersion}
150
+ latestVersion={latestVersion}
151
+ localVersion={localVersion}
152
+ overrideVersionHref={overrideVersionHref}
153
+ showVersionDetails={showVersionDetails}
154
+ currentLane={currentLane}
155
+ getActiveTabIndex={getActiveTabIndex}
156
+ lanes={lanes}
157
+ useVersions={useComponentVersions}
158
+ onVersionClicked={() => setOpen(false)}
159
+ open={open}
160
+ />
161
+ </Dropdown>
162
+ </div>
163
+ );
164
+ }
165
+
166
+ type VersionMenuProps = {
167
+ localVersion?: boolean;
168
+ currentVersion?: string;
169
+ latestVersion?: string;
170
+ useVersions?: UseComponentDropdownVersions;
171
+ currentLane?: LaneModel;
172
+ lanes?: LaneModel[];
173
+ overrideVersionHref?: (version: string) => string;
174
+ showVersionDetails?: boolean;
175
+ loading?: boolean;
176
+ getActiveTabIndex?: GetActiveTabIndex;
177
+ open?: boolean;
178
+ onVersionClicked?: () => void;
179
+ } & React.HTMLAttributes<HTMLDivElement>;
180
+
181
+ export type VersionMenuTab =
182
+ | {
183
+ name: 'SNAP';
184
+ payload: DropdownComponentVersion[];
185
+ }
186
+ | {
187
+ name: 'LANE';
188
+ payload: LaneModel[];
189
+ }
190
+ | {
191
+ name: 'TAG';
192
+ payload: DropdownComponentVersion[];
193
+ };
194
+
195
+ const defaultActiveTabIndex: GetActiveTabIndex = (currentVersion, tabs = [], tags, snaps) => {
196
+ if ((snaps || []).some((snap) => snap.version === currentVersion))
197
+ return tabs.findIndex((tab) => tab.name === 'SNAP');
198
+ return 0;
199
+ };
200
+
201
+ const VERSION_TAB_NAMES = ['TAG', 'SNAP', 'LANE'] as const;
202
+ function _VersionMenu({
203
+ currentVersion,
204
+ localVersion,
205
+ latestVersion,
206
+ overrideVersionHref,
207
+ showVersionDetails,
208
+ useVersions,
209
+ currentLane,
210
+ lanes,
211
+ getActiveTabIndex = defaultActiveTabIndex,
212
+ loading: loadingFromProps,
213
+ open,
214
+ onVersionClicked,
215
+ ...rest
216
+ }: VersionMenuProps) {
217
+ const { snaps, tags, loading: loadingVersions } = useVersions?.() || {};
218
+ const loading = loadingFromProps || loadingVersions;
219
+
220
+ const tabs = React.useMemo(
221
+ () =>
222
+ VERSION_TAB_NAMES.map((name) => {
223
+ switch (name) {
224
+ case 'SNAP':
225
+ return { name, payload: snaps || [] };
226
+ case 'LANE':
227
+ return { name, payload: lanes || [] };
228
+ default:
229
+ return { name, payload: tags || [] };
230
+ }
231
+ }).filter((tab) => tab.payload.length > 0),
232
+ [snaps?.length, tags?.length, lanes?.length, loading]
233
+ );
234
+
235
+ const [activeTabIndex, setActiveTab] = React.useState<number | undefined>(
236
+ getActiveTabIndex(currentVersion, tabs, tags, snaps, currentLane)
237
+ );
238
+
239
+ const activeTab = React.useMemo(
240
+ () => (activeTabIndex !== undefined ? tabs[activeTabIndex] : undefined),
241
+ [activeTabIndex, tabs]
242
+ );
243
+
244
+ React.useEffect(() => {
245
+ if (!currentLane) return;
246
+ if (tabs.length === 0) return;
247
+ const _activeTabIndex = getActiveTabIndex(currentVersion, tabs, tags, snaps, currentLane);
248
+ if (_activeTabIndex !== activeTabIndex) setActiveTab(_activeTabIndex);
249
+ }, [currentLane, tabs.length, tags?.length, snaps?.length, currentVersion, loading]);
250
+
251
+ const multipleTabs = tabs.length > 1;
252
+ const message = multipleTabs
253
+ ? 'Switch to view tags, snaps, or lanes'
254
+ : `Switch between ${tabs[0]?.name.toLocaleLowerCase()}s`;
255
+
256
+ const showTab = activeTabIndex !== undefined && tabs[activeTabIndex]?.payload.length > 0;
257
+
258
+ const _rowRenderer = React.useCallback(
259
+ function VersionRowRenderer({ index }) {
260
+ const { name, payload = [] } = activeTab || {};
261
+ const item = payload[index];
262
+ if (!item) return null;
263
+ if (name === 'LANE') {
264
+ const lane = item as LaneModel;
265
+ return <LaneInfo key={lane.id.toString()} currentLane={currentLane} {...lane}></LaneInfo>;
266
+ }
267
+ const version = item as DropdownComponentVersion;
268
+ return (
269
+ <VersionInfo
270
+ key={version.version}
271
+ currentVersion={currentVersion}
272
+ latestVersion={latestVersion}
273
+ overrideVersionHref={overrideVersionHref}
274
+ showDetails={showVersionDetails}
275
+ onVersionClicked={onVersionClicked}
276
+ {...version}
277
+ ></VersionInfo>
278
+ );
279
+ },
280
+ [activeTab, currentVersion, latestVersion, showVersionDetails, currentLane?.id.toString(), showTab]
281
+ );
282
+
283
+ const rowRenderer = React.useMemo(
284
+ () => (showTab && activeTab ? _rowRenderer : () => null),
285
+ [showTab, activeTab, _rowRenderer]
286
+ );
287
+
288
+ const ActiveTab = React.useMemo(() => {
289
+ return activeTab?.payload.map((payload, index) => {
290
+ return rowRenderer({ index });
291
+ });
292
+ }, [activeTab]);
293
+
294
+ return (
295
+ <div {...rest} className={classNames(styles.versionMenuContainer, !open && styles.hide)}>
296
+ <div className={styles.top}>
297
+ {loading && <LineSkeleton count={6} className={styles.loader} />}
298
+ {!loading && <div className={classNames(styles.titleContainer, styles.title)}>{message}</div>}
299
+ {!loading && localVersion && (
300
+ <MenuLinkItem
301
+ href={'?'}
302
+ active={currentVersion === LOCAL_VERSION}
303
+ className={classNames(styles.versionRow, styles.localVersion)}
304
+ onClick={onVersionClicked}
305
+ >
306
+ <div className={styles.version}>
307
+ <UserAvatar size={24} account={{}} className={styles.versionUserAvatar} />
308
+ <span className={styles.versionName}>{LOCAL_VERSION}</span>
309
+ </div>
310
+ </MenuLinkItem>
311
+ )}
312
+ </div>
313
+ <div className={classNames(multipleTabs && styles.tabs)}>
314
+ {multipleTabs &&
315
+ tabs.map(({ name }, index) => {
316
+ return (
317
+ <Tab
318
+ className={styles.tab}
319
+ key={name}
320
+ isActive={activeTabIndex === index}
321
+ onClick={() => setActiveTab(index)}
322
+ >
323
+ {name}
324
+ </Tab>
325
+ );
326
+ })}
327
+ </div>
328
+ <div className={styles.versionContainerRoot}>{ActiveTab}</div>
329
+ </div>
330
+ );
331
+ }
@@ -0,0 +1,2 @@
1
+ export { VersionInfo } from './version-info';
2
+ export type { VersionInfoProps } from './version-info';
@@ -0,0 +1,54 @@
1
+ .versionRow {
2
+ display: flex;
3
+ justify-content: space-between;
4
+ align-items: center;
5
+ height: 40px;
6
+ padding: 0 16px;
7
+ line-height: 1.2em;
8
+
9
+ &:hover {
10
+ background-color: var(--surface-hover-color, #eceaff) !important;
11
+ }
12
+ }
13
+
14
+ .versionTimestamp {
15
+ min-width: fit-content;
16
+ margin-right: 2px;
17
+ text-align: right;
18
+ }
19
+
20
+ .versionUserAvatar {
21
+ flex: none;
22
+ padding: 0px 8px;
23
+ }
24
+
25
+ .laneIcon {
26
+ padding: 0px 4px;
27
+ }
28
+
29
+ .version {
30
+ max-width: 70%;
31
+ display: flex;
32
+ flex-direction: row;
33
+ align-items: center;
34
+ justify-content: flex-start;
35
+ overflow: hidden;
36
+ }
37
+
38
+ .versionName {
39
+ padding: 0px 8px;
40
+ flex-grow: 1;
41
+ min-width: fit-content;
42
+ }
43
+
44
+ .commitMessage {
45
+ flex-grow: 1;
46
+ padding: 0px 8px;
47
+ }
48
+
49
+ .emptyMessage {
50
+ font-style: italic;
51
+ color: var(--bit-text-color-light, #6c707c);
52
+ padding: 0px 8px;
53
+ font-size: (var-bit-p-xs, 14px);
54
+ }
@@ -0,0 +1,87 @@
1
+ import { MenuLinkItem } from '@teambit/design.ui.surfaces.menu.link-item';
2
+ import { TimeAgo } from '@teambit/design.ui.time-ago';
3
+ import { VersionLabel } from '@teambit/component.ui.version-label';
4
+ import React, { useMemo, useRef, useEffect } from 'react';
5
+ import { UserAvatar } from '@teambit/design.ui.avatar';
6
+ import { Ellipsis } from '@teambit/design.ui.styles.ellipsis';
7
+ import classNames from 'classnames';
8
+
9
+ import type { DropdownComponentVersion } from '../version-dropdown';
10
+ import styles from './version-info.module.scss';
11
+
12
+ export interface VersionInfoProps extends DropdownComponentVersion {
13
+ currentVersion?: string;
14
+ latestVersion?: string;
15
+ overrideVersionHref?: (version: string) => string;
16
+ showDetails?: boolean;
17
+ onVersionClicked?: () => void;
18
+ }
19
+
20
+ export const VersionInfo = React.memo(React.forwardRef<HTMLDivElement, VersionInfoProps>(_VersionInfo));
21
+ function _VersionInfo(
22
+ {
23
+ version,
24
+ currentVersion,
25
+ latestVersion,
26
+ date,
27
+ username,
28
+ displayName,
29
+ email,
30
+ overrideVersionHref,
31
+ showDetails,
32
+ message,
33
+ tag,
34
+ profileImage,
35
+ onVersionClicked,
36
+ }: VersionInfoProps,
37
+ ref?: React.ForwardedRef<HTMLDivElement>
38
+ ) {
39
+ const isCurrent = version === currentVersion;
40
+ const author = useMemo(() => {
41
+ return {
42
+ displayName: displayName ?? '',
43
+ email,
44
+ name: username ?? '',
45
+ profileImage,
46
+ };
47
+ }, [displayName, email, username, profileImage]);
48
+
49
+ const timestamp = useMemo(() => (date ? new Date(parseInt(date)).toString() : new Date().toString()), [date]);
50
+ const currentVersionRef = useRef<HTMLDivElement>(null);
51
+
52
+ useEffect(() => {
53
+ if (isCurrent) {
54
+ currentVersionRef.current?.scrollIntoView({ block: 'nearest' });
55
+ }
56
+ }, [isCurrent]);
57
+
58
+ const href = overrideVersionHref ? overrideVersionHref(version) : `?version=${version}`;
59
+
60
+ const formattedVersion = useMemo(() => {
61
+ return tag ? version : version.slice(0, 6);
62
+ }, [tag, version]);
63
+
64
+ const isLatest = version === latestVersion;
65
+
66
+ return (
67
+ <div ref={ref || currentVersionRef} onClick={onVersionClicked}>
68
+ <MenuLinkItem active={isCurrent} href={href} className={styles.versionRow}>
69
+ <div className={styles.version}>
70
+ <UserAvatar size={24} account={author} className={styles.versionUserAvatar} showTooltip={true} />
71
+ <Ellipsis className={classNames(styles.versionName)}>{formattedVersion}</Ellipsis>
72
+ {isLatest && <VersionLabel status="latest" />}
73
+ <CommitMessage message={message} showDetails={showDetails} />
74
+ </div>
75
+ <Ellipsis className={styles.versionTimestamp}>
76
+ <TimeAgo date={timestamp} />
77
+ </Ellipsis>
78
+ </MenuLinkItem>
79
+ </div>
80
+ );
81
+ }
82
+
83
+ function CommitMessage({ message, showDetails }: { message?: string; showDetails?: boolean }) {
84
+ if (!showDetails) return null;
85
+ if (!message || message === '') return <Ellipsis className={styles.emptyMessage}>No commit message</Ellipsis>;
86
+ return <Ellipsis className={styles.commitMessage}>{message}</Ellipsis>;
87
+ }