@teambit/workspace 1.0.844 → 1.0.846

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 (35) hide show
  1. package/dist/{preview-1765488834493.js → preview-1765712510733.js} +2 -2
  2. package/dist/ui/workspace/workspace-overview/filter-utils.d.ts +7 -0
  3. package/dist/ui/workspace/workspace-overview/filter-utils.js +24 -0
  4. package/dist/ui/workspace/workspace-overview/filter-utils.js.map +1 -0
  5. package/dist/ui/workspace/workspace-overview/namespace-sort.d.ts +8 -0
  6. package/dist/ui/workspace/workspace-overview/namespace-sort.js +44 -0
  7. package/dist/ui/workspace/workspace-overview/namespace-sort.js.map +1 -0
  8. package/dist/ui/workspace/workspace-overview/scope-sort.d.ts +1 -0
  9. package/dist/ui/workspace/workspace-overview/scope-sort.js +14 -0
  10. package/dist/ui/workspace/workspace-overview/scope-sort.js.map +1 -0
  11. package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.d.ts +2 -0
  12. package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.js +84 -0
  13. package/dist/ui/workspace/workspace-overview/use-workspace-aggregation.js.map +1 -0
  14. package/dist/ui/workspace/workspace-overview/workspace-filter-panel.d.ts +7 -0
  15. package/dist/ui/workspace/workspace-overview/workspace-filter-panel.js +113 -0
  16. package/dist/ui/workspace/workspace-overview/workspace-filter-panel.js.map +1 -0
  17. package/dist/ui/workspace/workspace-overview/workspace-overview.js +91 -49
  18. package/dist/ui/workspace/workspace-overview/workspace-overview.js.map +1 -1
  19. package/dist/ui/workspace/workspace-overview/workspace-overview.module.scss +166 -12
  20. package/dist/ui/workspace/workspace-overview/workspace-overview.sort.d.ts +1 -0
  21. package/dist/ui/workspace/workspace-overview/workspace-overview.sort.js +30 -0
  22. package/dist/ui/workspace/workspace-overview/workspace-overview.sort.js.map +1 -0
  23. package/dist/ui/workspace/workspace-overview/workspace-overview.types.d.ts +22 -0
  24. package/dist/ui/workspace/workspace-overview/workspace-overview.types.js +3 -0
  25. package/dist/ui/workspace/workspace-overview/workspace-overview.types.js.map +1 -0
  26. package/package.json +22 -19
  27. package/ui/workspace/workspace-overview/filter-utils.ts +25 -0
  28. package/ui/workspace/workspace-overview/namespace-sort.ts +41 -0
  29. package/ui/workspace/workspace-overview/scope-sort.ts +9 -0
  30. package/ui/workspace/workspace-overview/use-workspace-aggregation.ts +72 -0
  31. package/ui/workspace/workspace-overview/workspace-filter-panel.tsx +112 -0
  32. package/ui/workspace/workspace-overview/workspace-overview.module.scss +166 -12
  33. package/ui/workspace/workspace-overview/workspace-overview.sort.ts +29 -0
  34. package/ui/workspace/workspace-overview/workspace-overview.tsx +53 -32
  35. package/ui/workspace/workspace-overview/workspace-overview.types.ts +24 -0
@@ -0,0 +1,112 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+ import { ToggleButton } from '@teambit/design.inputs.toggle-button';
4
+ import { BaseFilter } from '@teambit/component.filters.base-filter';
5
+ import type { WorkspaceItem, AggregationType } from './workspace-overview.types';
6
+ import styles from './workspace-overview.module.scss';
7
+
8
+ export interface WorkspaceFilterPanelProps {
9
+ aggregation: AggregationType;
10
+ availableAggregations: AggregationType[];
11
+ items: WorkspaceItem[];
12
+ }
13
+
14
+ const LABELS: Record<AggregationType, string> = {
15
+ namespaces: 'Namespaces',
16
+ scopes: 'Scopes',
17
+ none: 'None',
18
+ };
19
+
20
+ export function WorkspaceFilterPanel({ aggregation, availableAggregations, items }: WorkspaceFilterPanelProps) {
21
+ const [searchParams, setSearchParams] = useSearchParams();
22
+
23
+ const namespaceOptions = useMemo(
24
+ () =>
25
+ [...new Set(items.map((i) => i.component.id.namespace || '/'))].map((v) => ({
26
+ value: v,
27
+ })),
28
+ [items]
29
+ );
30
+
31
+ const scopeOptions = useMemo(
32
+ () =>
33
+ [...new Set(items.map((i) => i.component.id.scope))].map((v) => ({
34
+ value: v,
35
+ })),
36
+ [items]
37
+ );
38
+
39
+ const activeNamespaces = (searchParams.get('ns') || '')
40
+ .split(',')
41
+ .filter(Boolean)
42
+ .map((v) => ({ value: v }));
43
+
44
+ const activeScopes = (searchParams.get('scopes') || '')
45
+ .split(',')
46
+ .filter(Boolean)
47
+ .map((v) => ({ value: v }));
48
+
49
+ const applyNs = (opts) => {
50
+ const list = opts.map((o) => o.value).filter((v): v is string => typeof v === 'string');
51
+
52
+ if (list.length) searchParams.set('ns', list.join(','));
53
+ else searchParams.delete('ns');
54
+
55
+ setSearchParams(searchParams);
56
+ };
57
+
58
+ const applyScopes = (opts) => {
59
+ const list = opts.map((o) => o.value).filter((v): v is string => typeof v === 'string');
60
+
61
+ if (list.length) searchParams.set('scopes', list.join(','));
62
+ else searchParams.delete('scopes');
63
+
64
+ setSearchParams(searchParams);
65
+ };
66
+
67
+ const currentIndex = Math.max(
68
+ 0,
69
+ availableAggregations.findIndex((a) => a === aggregation)
70
+ );
71
+
72
+ const applyAgg = (i: number) => {
73
+ const agg = availableAggregations[i];
74
+ searchParams.set('aggregation', agg);
75
+ setSearchParams(searchParams);
76
+ };
77
+
78
+ return (
79
+ <div className={styles.filterPanel}>
80
+ <div className={styles.leftFilters}>
81
+ <BaseFilter
82
+ id="namespaces"
83
+ placeholder="Namespaces"
84
+ options={namespaceOptions}
85
+ activeOptions={activeNamespaces}
86
+ onChange={applyNs}
87
+ isSearchable
88
+ />
89
+
90
+ <BaseFilter
91
+ id="scopes"
92
+ placeholder="Scopes"
93
+ options={scopeOptions}
94
+ activeOptions={activeScopes}
95
+ onChange={applyScopes}
96
+ isSearchable
97
+ />
98
+ </div>
99
+
100
+ <div className={styles.rightAggToggle}>
101
+ <ToggleButton
102
+ defaultIndex={currentIndex}
103
+ onOptionSelect={(idx) => applyAgg(idx)}
104
+ options={availableAggregations.map((agg) => ({
105
+ value: agg,
106
+ element: LABELS[agg],
107
+ }))}
108
+ />
109
+ </div>
110
+ </div>
111
+ );
112
+ }
@@ -3,15 +3,11 @@
3
3
  overflow-y: auto;
4
4
  height: 100%;
5
5
  box-sizing: border-box;
6
+ z-index: 1;
6
7
  }
7
8
 
8
9
  .rightPreviewPlugins {
9
- // background-color: var(--surface-color);
10
10
  display: flex;
11
- // box-shadow: 0px 2px 48px rgb(0 0 0 / 89%);
12
- // box-shadow: 0px 2px 160px rgb(0 0 0 / 52%);
13
- // border-radius: 8px;
14
- // padding: 4px;
15
11
  align-items: flex-end;
16
12
  justify-content: flex-end;
17
13
  width: 100%;
@@ -19,10 +15,7 @@
19
15
  }
20
16
 
21
17
  .envIcon {
22
- // padding-right: 4px;
23
- // height: 24px;
24
18
  height: 14px;
25
- // padding-top: 2px;
26
19
  }
27
20
 
28
21
  .badge {
@@ -37,12 +30,173 @@
37
30
  }
38
31
 
39
32
  .cardGrid {
40
- grid-column-gap: 10px;
41
- grid-gap: 32px 24px;
42
- -moz-column-gap: 10px;
43
- column-gap: 10px;
44
33
  display: grid;
45
34
  gap: 32px 24px;
46
35
  grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
47
36
  max-width: 1280px;
37
+ grid-column-gap: 28px;
38
+ grid-row-gap: 32px;
39
+ }
40
+
41
+ .filterPanel {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: space-between;
45
+ margin-bottom: 24px;
46
+ position: relative;
47
+ z-index: 100000;
48
+ }
49
+
50
+ .aggButtons {
51
+ display: inline-flex;
52
+ background: var(--surface-1-color);
53
+ border: 1px solid var(--border-medium-color);
54
+ border-radius: 8px;
55
+ padding: 2px;
56
+ height: 34px;
57
+ }
58
+
59
+ .aggButton,
60
+ .aggActive {
61
+ padding: 4px 12px;
62
+ font-size: 14px;
63
+ border-radius: 6px;
64
+ background: transparent;
65
+ cursor: pointer;
66
+ border: none;
67
+ display: flex;
68
+ align-items: center;
69
+ }
70
+
71
+ .aggButton:hover {
72
+ background: var(--surface-hover-color);
73
+ }
74
+
75
+ .aggActive {
76
+ background: var(--brand-primary-color);
77
+ color: white;
78
+ }
79
+
80
+ .dropdownList {
81
+ max-height: 260px;
82
+ overflow-y: auto;
83
+ padding: 0 12px;
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 8px;
87
+ }
88
+
89
+ .dropdownItem {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 8px;
93
+ font-size: 14px;
94
+ cursor: pointer;
95
+ }
96
+
97
+ .dropdownItem input {
98
+ width: 16px;
99
+ height: 16px;
100
+ }
101
+
102
+ .count {
103
+ opacity: 0.55;
104
+ }
105
+
106
+ .agg {
107
+ margin-bottom: 28px;
108
+ }
109
+
110
+ .aggregationTitle {
111
+ margin-top: 0;
112
+ margin-bottom: 15px;
113
+ }
114
+
115
+ :global(.componentGrid) {
116
+ grid-row-gap: 24px !important;
117
+ grid-column-gap: 16px !important;
118
+ }
119
+
120
+ .filterPanel :global(.baseFilter) {
121
+ max-width: 220px;
122
+ height: 32px;
123
+ }
124
+
125
+ .filterPanel :global(.control) {
126
+ border-radius: 8px !important;
127
+ height: 32px;
128
+ padding: 0 10px !important;
129
+ }
130
+
131
+ .filterPanel :global(.menu) {
132
+ z-index: 200000 !important;
133
+ border-radius: 8px !important;
134
+ padding: 8px 0 !important;
135
+ }
136
+
137
+ .filterDropdown :global(.baseFilter) {
138
+ height: 32px;
139
+ max-width: 240px;
140
+ font-size: 14px;
141
+ }
142
+
143
+ .filterDropdown :global(.baseFilter .control) {
144
+ padding: 8px 12px;
145
+ border-radius: 8px;
146
+ }
147
+
148
+ .filterDropdown :global(.menu) {
149
+ margin-top: 10px;
150
+ padding: 12px 0;
151
+ min-width: 260px;
152
+ border-radius: 12px;
153
+
154
+ box-shadow: var(--bit-shadow-hover-low, 0 2px 8px rgba(0, 0, 0, 0.1));
155
+ border: 1px solid var(--border-medium-color, #ededed);
156
+ }
157
+
158
+ .filterDropdown :global(.checkboxContainer) {
159
+ padding-left: 14px;
160
+ }
161
+
162
+ .filterDropdown :global(.checkbox) {
163
+ margin-right: 12px;
164
+ accent-color: var(--bit-accent-color, #6c5ce7);
165
+ }
166
+
167
+ .filterDropdown :global(.buttonsSection) {
168
+ padding: 12px 16px;
169
+ border-top: 1px solid var(--border-medium-color, #ededed);
170
+ }
171
+
172
+ .filterDropdown :global(.placeholder > span:not(:first-child)) {
173
+ margin-left: 8px;
174
+ }
175
+
176
+ .filterPanel {
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: space-between;
180
+ margin-bottom: 24px;
181
+ gap: 16px;
182
+ }
183
+
184
+ .leftFilters {
185
+ display: flex;
186
+ gap: 12px;
187
+ align-items: center;
188
+ }
189
+
190
+ .rightAggToggle {
191
+ display: flex;
192
+ align-items: center;
193
+ margin-left: auto;
194
+ }
195
+
196
+ .cardGrid {
197
+ display: grid;
198
+ grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
199
+ grid-row-gap: 32px;
200
+ grid-column-gap: 24px;
201
+ max-width: 1280px;
48
202
  }
@@ -0,0 +1,29 @@
1
+ const PRIORITY_HIGH = ['ui', 'pages'];
2
+ const PRIORITY_MED = ['design'];
3
+ const PRIORITY_LOW = ['entities', 'provider', 'hooks', 'icons'];
4
+
5
+ function root(ns: string) {
6
+ if (!ns) return '';
7
+ return ns.split('/')[0];
8
+ }
9
+
10
+ function priorityOf(ns: string) {
11
+ const r = root(ns);
12
+
13
+ if (PRIORITY_HIGH.includes(r)) return 0;
14
+ if (PRIORITY_MED.includes(r)) return 1;
15
+ if (PRIORITY_LOW.includes(r)) return 3;
16
+
17
+ return 2;
18
+ }
19
+
20
+ export function sortNamespacesAdvanced(arr: string[]) {
21
+ return [...arr].sort((a, b) => {
22
+ const pa = priorityOf(a);
23
+ const pb = priorityOf(b);
24
+
25
+ if (pa !== pb) return pa - pb;
26
+
27
+ return a.localeCompare(b);
28
+ });
29
+ }
@@ -1,66 +1,87 @@
1
- import React, { useContext } from 'react';
1
+ import React, { useContext, useMemo } from 'react';
2
2
  import { ComponentGrid } from '@teambit/explorer.ui.gallery.component-grid';
3
3
  import { EmptyWorkspace } from '@teambit/workspace.ui.empty-workspace';
4
4
  import { PreviewPlaceholder } from '@teambit/preview.ui.preview-placeholder';
5
5
  import { Tooltip } from '@teambit/design.ui.tooltip';
6
6
  import { ComponentID } from '@teambit/component-id';
7
7
  import type { ComponentModel } from '@teambit/component';
8
- import { useCloudScopes } from '@teambit/cloud.hooks.use-cloud-scopes';
8
+ import compact from 'lodash.compact';
9
9
  import { ScopeID } from '@teambit/scopes.scope-id';
10
- import { compact } from 'lodash';
10
+ import { useCloudScopes } from '@teambit/cloud.hooks.use-cloud-scopes';
11
+ import { useSearchParams } from 'react-router-dom';
11
12
  import { WorkspaceComponentCard } from '@teambit/workspace.ui.workspace-component-card';
12
13
  import type { ComponentCardPluginType, PluginProps } from '@teambit/explorer.ui.component-card';
13
14
  import { useWorkspaceMode } from '@teambit/workspace.ui.use-workspace-mode';
15
+ import { H3 } from '@teambit/design.ui.heading';
14
16
  import { WorkspaceContext } from '../workspace-context';
15
- import styles from './workspace-overview.module.scss';
16
17
  import { LinkPlugin } from './link-plugin';
18
+ import { useWorkspaceAggregation } from './use-workspace-aggregation';
19
+ import { WorkspaceFilterPanel } from './workspace-filter-panel';
20
+ import styles from './workspace-overview.module.scss';
17
21
 
18
22
  export function WorkspaceOverview() {
19
23
  const workspace = useContext(WorkspaceContext);
20
- const { isMinimal } = useWorkspaceMode();
21
- const compModelsById = new Map(workspace.components.map((comp) => [comp.id.toString(), comp]));
22
24
  const { components, componentDescriptors } = workspace;
23
- const uniqueScopes = new Set(components.map((c) => c.id.scope));
24
- const uniqueScopesArr = Array.from(uniqueScopes);
25
- const { cloudScopes = [] } = useCloudScopes(uniqueScopesArr);
26
- const cloudScopesById = new Map(cloudScopes.map((scope) => [scope.id.toString(), scope]));
27
25
 
28
- const plugins = useCardPlugins({ compModelsById, showPreview: isMinimal });
26
+ if (!components.length) return <EmptyWorkspace name={workspace.name} />;
29
27
 
30
- if (!components || components.length === 0) return <EmptyWorkspace name={workspace.name} />;
28
+ const { isMinimal } = useWorkspaceMode();
29
+ const compModelsById = useMemo(() => new Map(components.map((c) => [c.id.toString(), c])), [components]);
30
+
31
+ const uniqueScopes = [...new Set(components.map((c) => c.id.scope))];
32
+ const { cloudScopes } = useCloudScopes(uniqueScopes);
33
+ const cloudMap = new Map((cloudScopes || []).map((s) => [s.id.toString(), s]));
34
+
35
+ const compDescriptorMap = new Map(componentDescriptors.map((d) => [d.id.toString(), d]));
31
36
 
32
- const compDescriptorById = new Map(componentDescriptors.map((comp) => [comp.id.toString(), comp]));
33
- const componentsWithDescriptorAndScope = compact(
37
+ const items = compact(
34
38
  components.map((component) => {
35
39
  if (component.deprecation?.isDeprecate) return null;
36
- const componentDescriptor = compDescriptorById.get(component.id.toString());
37
- if (!componentDescriptor) return null;
38
- const cloudScope = cloudScopesById.get(component.id.scope);
40
+
41
+ const descriptor = compDescriptorMap.get(component.id.toString());
42
+ if (!descriptor) return null;
43
+
44
+ const cloudScope = cloudMap.get(component.id.scope);
39
45
  const scope =
40
46
  cloudScope ||
41
47
  (ScopeID.isValid(component.id.scope) && { id: ScopeID.fromString(component.id.scope) }) ||
42
48
  undefined;
43
49
 
44
- return { component, componentDescriptor, scope };
50
+ return { component, componentDescriptor: descriptor, scope: (scope && { id: scope.id }) || undefined };
45
51
  })
46
52
  );
47
53
 
54
+ const [searchParams] = useSearchParams();
55
+ const aggregation = (searchParams.get('aggregation') as any) || 'namespaces';
56
+
57
+ const { groups, groupType, availableAggregations, filteredCount } = useWorkspaceAggregation(items, aggregation);
58
+
59
+ const plugins = useCardPlugins({ compModelsById, showPreview: isMinimal });
60
+
48
61
  return (
49
62
  <div className={styles.container}>
50
- <ComponentGrid className={styles.cardGrid}>
51
- {componentsWithDescriptorAndScope.map(({ component, componentDescriptor, scope }) => {
52
- return (
53
- <WorkspaceComponentCard
54
- key={component.id.toString()}
55
- componentDescriptor={componentDescriptor}
56
- component={component}
57
- plugins={plugins}
58
- scope={scope}
59
- shouldShowPreviewState={isMinimal}
60
- />
61
- );
62
- })}
63
- </ComponentGrid>
63
+ <WorkspaceFilterPanel aggregation={aggregation} availableAggregations={availableAggregations} items={items} />
64
+
65
+ {filteredCount === 0 && <EmptyWorkspace name={workspace.name} />}
66
+
67
+ {groups.map((group) => (
68
+ <section key={group.name} className={styles.agg}>
69
+ {groupType !== 'none' && <H3 className={styles.aggregationTitle}>{group.displayName}</H3>}
70
+
71
+ <ComponentGrid className={styles.cardGrid}>
72
+ {group.items.map((item) => (
73
+ <WorkspaceComponentCard
74
+ key={item.component.id.toString()}
75
+ component={item.component}
76
+ componentDescriptor={item.componentDescriptor}
77
+ scope={item.scope}
78
+ plugins={plugins}
79
+ shouldShowPreviewState={isMinimal}
80
+ />
81
+ ))}
82
+ </ComponentGrid>
83
+ </section>
84
+ ))}
64
85
  </div>
65
86
  );
66
87
  }
@@ -0,0 +1,24 @@
1
+ import type { ComponentModel } from '@teambit/component';
2
+ import type { ComponentDescriptor } from '@teambit/component-descriptor';
3
+ import type { ScopeID } from '@teambit/scopes.scope-id';
4
+
5
+ export interface WorkspaceItem {
6
+ component: ComponentModel;
7
+ componentDescriptor: ComponentDescriptor;
8
+ scope?: { id: ScopeID };
9
+ }
10
+
11
+ export type AggregationType = 'namespaces' | 'scopes' | 'none';
12
+
13
+ export interface AggregationGroup {
14
+ name: string;
15
+ displayName: string;
16
+ items: WorkspaceItem[];
17
+ }
18
+
19
+ export interface AggregationResult {
20
+ groups: AggregationGroup[];
21
+ groupType: AggregationType;
22
+ availableAggregations: AggregationType[];
23
+ filteredCount: number;
24
+ }