@design.estate/dees-catalog 6.6.0 → 6.7.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.
- package/dist_bundle/bundle.js +1481 -657
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +41 -2
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +100 -2
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +816 -28
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +56 -0
- package/package.json +1 -1
- package/readme.hints.md +3 -0
- package/readme.md +49 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +123 -1
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +922 -28
- package/ts_web/elements/00group-harness/interfaces.ts +67 -0
package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts
CHANGED
|
@@ -13,10 +13,17 @@ import {
|
|
|
13
13
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
14
14
|
import type {
|
|
15
15
|
IHarnessGroupContextDetail,
|
|
16
|
+
IHarnessResourceAssociation,
|
|
17
|
+
IHarnessResourceContextDetail,
|
|
18
|
+
IHarnessResourceEventDetail,
|
|
19
|
+
IHarnessResourceMeta,
|
|
16
20
|
IHarnessSessionContextDetail,
|
|
17
21
|
IHarnessSessionGroup,
|
|
18
22
|
IHarnessSessionMeta,
|
|
19
23
|
IHarnessSessionMoveDetail,
|
|
24
|
+
THarnessResourceAssociationInteractionSource,
|
|
25
|
+
THarnessResourceAssociationRequestDetail,
|
|
26
|
+
THarnessResourceContextSource,
|
|
20
27
|
} from '../interfaces.js';
|
|
21
28
|
import { harnessFormatRelativeTime } from '../harness.helpers.js';
|
|
22
29
|
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
@@ -37,6 +44,7 @@ interface IDropSlot {
|
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
interface IPointerDragGesture {
|
|
47
|
+
type: 'session';
|
|
40
48
|
pointerId: number;
|
|
41
49
|
handle: HTMLElement;
|
|
42
50
|
source: HTMLElement;
|
|
@@ -50,6 +58,30 @@ interface IPointerDragGesture {
|
|
|
50
58
|
phase: 'pending' | 'active';
|
|
51
59
|
}
|
|
52
60
|
|
|
61
|
+
interface IResourcePointerDragGesture {
|
|
62
|
+
type: 'resource';
|
|
63
|
+
pointerId: number;
|
|
64
|
+
handle: HTMLElement;
|
|
65
|
+
source: HTMLElement;
|
|
66
|
+
resourceId: string;
|
|
67
|
+
fromSessionId: string | null;
|
|
68
|
+
startClientX: number;
|
|
69
|
+
startClientY: number;
|
|
70
|
+
grabOffsetX: number;
|
|
71
|
+
grabOffsetY: number;
|
|
72
|
+
phase: 'pending' | 'active';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type TPointerDragGesture = IPointerDragGesture | IResourcePointerDragGesture;
|
|
76
|
+
|
|
77
|
+
interface IPendingResourceAssociationRequest {
|
|
78
|
+
resourceId: string;
|
|
79
|
+
resourceTitle: string;
|
|
80
|
+
fromSessionId: string | null;
|
|
81
|
+
toSessionId: string | null;
|
|
82
|
+
intent: THarnessResourceAssociationRequestDetail['intent'];
|
|
83
|
+
}
|
|
84
|
+
|
|
53
85
|
interface IKeyboardMoveFocus {
|
|
54
86
|
sessionId: string;
|
|
55
87
|
groupId: string | null;
|
|
@@ -72,7 +104,9 @@ type TSectionRenderEntry =
|
|
|
72
104
|
* `harness-group-context` — the host persists changes and passes fresh
|
|
73
105
|
* `groups` back in. Sessions in no group render in the trailing ungrouped
|
|
74
106
|
* section with pinned entries first, then explicit order, then new sessions by
|
|
75
|
-
* recency. While a search query is active the list is flat.
|
|
107
|
+
* recency. While a search query is active the list is flat. Generic resources
|
|
108
|
+
* project as controlled child rows or into a trailing unattached Resources
|
|
109
|
+
* section; association interactions only emit host requests.
|
|
76
110
|
*/
|
|
77
111
|
@customElement('dees-harness-session-list')
|
|
78
112
|
export class DeesHarnessSessionList extends DeesElement {
|
|
@@ -101,12 +135,31 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
101
135
|
@property({ attribute: false })
|
|
102
136
|
accessor ungroupedSessionIds: string[] = [];
|
|
103
137
|
|
|
138
|
+
@property({ attribute: false })
|
|
139
|
+
accessor resources: IHarnessResourceMeta[] = [];
|
|
140
|
+
|
|
141
|
+
@property({ attribute: false })
|
|
142
|
+
accessor resourceAssociations: IHarnessResourceAssociation[] = [];
|
|
143
|
+
|
|
144
|
+
@property()
|
|
145
|
+
accessor selectedResourceId = '';
|
|
146
|
+
|
|
147
|
+
/** Serializable attach/reassign targets. A missing resource entry means no eligible targets. */
|
|
148
|
+
@property({ attribute: false })
|
|
149
|
+
accessor eligibleSessionIdsByResourceId: Record<string, string[]> = {};
|
|
150
|
+
|
|
104
151
|
@state()
|
|
105
152
|
accessor search: string = '';
|
|
106
153
|
|
|
107
154
|
@state()
|
|
108
155
|
accessor draggingSessionId: string = '';
|
|
109
156
|
|
|
157
|
+
@state()
|
|
158
|
+
accessor draggingResourceId = '';
|
|
159
|
+
|
|
160
|
+
@state()
|
|
161
|
+
accessor resourceDropTargetSessionId = '';
|
|
162
|
+
|
|
110
163
|
@state()
|
|
111
164
|
accessor dropSlot: IDropSlot | undefined = undefined;
|
|
112
165
|
|
|
@@ -128,9 +181,16 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
128
181
|
@state()
|
|
129
182
|
private accessor moveAnnouncement = '';
|
|
130
183
|
|
|
184
|
+
@state()
|
|
185
|
+
private accessor resourceContextResourceId = '';
|
|
186
|
+
|
|
131
187
|
private reflowGeneration = 0;
|
|
132
188
|
private readonly reflowAnimations = new Set<Animation>();
|
|
133
|
-
private pointerDragGesture:
|
|
189
|
+
private pointerDragGesture: TPointerDragGesture | undefined;
|
|
190
|
+
private pendingResourceAssociationRequest: IPendingResourceAssociationRequest | undefined;
|
|
191
|
+
private resourceContextFocusTarget: HTMLElement | undefined;
|
|
192
|
+
private resourceContextFocusClass = '';
|
|
193
|
+
private resourceContextGeneration = 0;
|
|
134
194
|
private keyboardMoveFocus: IKeyboardMoveFocus | undefined;
|
|
135
195
|
private scrollResizeObserver: ResizeObserver | undefined;
|
|
136
196
|
private observedScrollContainer: HTMLElement | undefined;
|
|
@@ -387,6 +447,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
387
447
|
--card-accent: var(--session-error);
|
|
388
448
|
}
|
|
389
449
|
|
|
450
|
+
.item.resourceDropTarget {
|
|
451
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
452
|
+
outline-offset: 2px;
|
|
453
|
+
box-shadow: 0 0 0 5px var(--dees-color-focus-ring), var(--dees-shadow-lg);
|
|
454
|
+
}
|
|
455
|
+
|
|
390
456
|
@keyframes attentionPulse {
|
|
391
457
|
0%, 100% {
|
|
392
458
|
background: color-mix(in srgb, var(--dees-color-bg-primary) 94%, var(--session-attention) 6%);
|
|
@@ -561,6 +627,183 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
561
627
|
font-size: var(--dees-font-caption2-size, 10px);
|
|
562
628
|
}
|
|
563
629
|
|
|
630
|
+
.resourceDropLabel {
|
|
631
|
+
margin: 0 10px 10px;
|
|
632
|
+
padding: 6px 9px;
|
|
633
|
+
border-radius: var(--dees-radius-md);
|
|
634
|
+
background: color-mix(in srgb, var(--dees-color-accent-primary) 14%, transparent);
|
|
635
|
+
color: var(--dees-color-accent-primary);
|
|
636
|
+
font-size: var(--dees-font-caption1-size, 11px);
|
|
637
|
+
font-weight: 680;
|
|
638
|
+
text-align: center;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.resourceChildren {
|
|
642
|
+
display: grid;
|
|
643
|
+
gap: 4px;
|
|
644
|
+
margin: 0 8px 8px 24px;
|
|
645
|
+
padding: 0;
|
|
646
|
+
list-style: none;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
.resourceSection {
|
|
650
|
+
margin-top: var(--dees-spacing-md);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
.resourceSectionTitle {
|
|
654
|
+
margin: 0 0 7px;
|
|
655
|
+
padding: var(--dees-spacing-xs) 6px;
|
|
656
|
+
color: var(--dees-color-text-secondary);
|
|
657
|
+
font-size: var(--dees-font-caption1-size, 11px);
|
|
658
|
+
font-weight: 650;
|
|
659
|
+
letter-spacing: 0.05em;
|
|
660
|
+
text-transform: uppercase;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
.resourceList {
|
|
664
|
+
display: grid;
|
|
665
|
+
gap: var(--dees-spacing-sm);
|
|
666
|
+
margin: 0;
|
|
667
|
+
padding: 0;
|
|
668
|
+
list-style: none;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.resourceRow {
|
|
672
|
+
display: grid;
|
|
673
|
+
grid-template-columns: 26px minmax(0, 1fr) 38px;
|
|
674
|
+
min-width: 0;
|
|
675
|
+
overflow: hidden;
|
|
676
|
+
border: 1px solid var(--dees-color-border-subtle);
|
|
677
|
+
border-radius: var(--dees-radius-lg);
|
|
678
|
+
background: color-mix(in srgb, var(--dees-color-bg-secondary) 76%, transparent);
|
|
679
|
+
color: var(--dees-color-text-primary);
|
|
680
|
+
box-shadow: var(--dees-shadow-xs);
|
|
681
|
+
box-sizing: border-box;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
.resourceRow.selected {
|
|
685
|
+
border-color: var(--dees-color-accent-primary);
|
|
686
|
+
background: color-mix(in srgb, var(--dees-color-accent-primary) 10%, var(--dees-color-bg-secondary));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.resourceRow.resourceDragging {
|
|
690
|
+
position: fixed;
|
|
691
|
+
inset: 0 auto auto 0;
|
|
692
|
+
width: var(--resource-drag-preview-width);
|
|
693
|
+
max-width: none;
|
|
694
|
+
margin: 0;
|
|
695
|
+
opacity: 0.96;
|
|
696
|
+
pointer-events: none;
|
|
697
|
+
transform: translate3d(var(--resource-drag-preview-x), var(--resource-drag-preview-y), 0);
|
|
698
|
+
box-shadow: var(--dees-shadow-xl);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.resourceDragHandle,
|
|
702
|
+
.resourcePrimary,
|
|
703
|
+
.resourceActions {
|
|
704
|
+
appearance: none;
|
|
705
|
+
border: 0;
|
|
706
|
+
background: transparent;
|
|
707
|
+
color: inherit;
|
|
708
|
+
font-family: inherit;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.resourceDragHandle {
|
|
712
|
+
display: grid;
|
|
713
|
+
place-items: center;
|
|
714
|
+
min-width: 26px;
|
|
715
|
+
padding: 0;
|
|
716
|
+
color: var(--dees-color-text-muted);
|
|
717
|
+
cursor: grab;
|
|
718
|
+
touch-action: none;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
.resourceDragHandle:disabled {
|
|
722
|
+
opacity: 0.35;
|
|
723
|
+
cursor: default;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
.resourceDragHandle:not(:disabled):active {
|
|
727
|
+
cursor: grabbing;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.resourcePrimary {
|
|
731
|
+
display: flex;
|
|
732
|
+
min-width: 0;
|
|
733
|
+
min-height: 38px;
|
|
734
|
+
align-items: center;
|
|
735
|
+
gap: 7px;
|
|
736
|
+
padding: 5px 4px 5px 7px;
|
|
737
|
+
text-align: left;
|
|
738
|
+
cursor: pointer;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.resourceIcon {
|
|
742
|
+
flex: 0 0 auto;
|
|
743
|
+
color: var(--dees-color-text-secondary);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.resourceCopy {
|
|
747
|
+
min-width: 0;
|
|
748
|
+
flex: 1;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.resourceTitle {
|
|
752
|
+
display: block;
|
|
753
|
+
overflow: hidden;
|
|
754
|
+
color: var(--dees-color-text-primary);
|
|
755
|
+
font-size: var(--dees-font-caption1-size, 11px);
|
|
756
|
+
font-weight: 620;
|
|
757
|
+
text-overflow: ellipsis;
|
|
758
|
+
white-space: nowrap;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
.resourcePreview {
|
|
762
|
+
display: block;
|
|
763
|
+
overflow: hidden;
|
|
764
|
+
margin-top: 1px;
|
|
765
|
+
color: var(--dees-color-text-secondary);
|
|
766
|
+
font-size: var(--dees-font-caption2-size, 10px);
|
|
767
|
+
text-overflow: ellipsis;
|
|
768
|
+
white-space: nowrap;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.resourceMeta {
|
|
772
|
+
display: block;
|
|
773
|
+
overflow: hidden;
|
|
774
|
+
margin-top: 1px;
|
|
775
|
+
color: var(--dees-color-text-muted);
|
|
776
|
+
font-size: var(--dees-font-caption2-size, 10px);
|
|
777
|
+
text-overflow: ellipsis;
|
|
778
|
+
white-space: nowrap;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
.resourceActions {
|
|
782
|
+
display: grid;
|
|
783
|
+
place-items: center;
|
|
784
|
+
min-width: 34px;
|
|
785
|
+
min-height: 34px;
|
|
786
|
+
margin: 2px;
|
|
787
|
+
padding: 0;
|
|
788
|
+
border-radius: var(--dees-radius-md);
|
|
789
|
+
color: var(--dees-color-text-muted);
|
|
790
|
+
cursor: pointer;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.resourceActions:hover {
|
|
794
|
+
background: var(--dees-color-hover);
|
|
795
|
+
color: var(--dees-color-text-primary);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.resourceDragHandle:focus-visible,
|
|
799
|
+
.resourcePrimary:focus-visible,
|
|
800
|
+
.resourceActions:focus-visible {
|
|
801
|
+
z-index: 1;
|
|
802
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
803
|
+
outline-offset: -3px;
|
|
804
|
+
border-radius: var(--dees-radius-md);
|
|
805
|
+
}
|
|
806
|
+
|
|
564
807
|
.visuallyHidden {
|
|
565
808
|
position: absolute;
|
|
566
809
|
width: 1px;
|
|
@@ -687,6 +930,33 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
687
930
|
transition: none;
|
|
688
931
|
}
|
|
689
932
|
}
|
|
933
|
+
|
|
934
|
+
@media (pointer: coarse) {
|
|
935
|
+
.resourceRow {
|
|
936
|
+
grid-template-columns: 44px minmax(0, 1fr) 44px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.resourceDragHandle,
|
|
940
|
+
.resourceActions {
|
|
941
|
+
min-width: 44px;
|
|
942
|
+
min-height: 44px;
|
|
943
|
+
margin: 0;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
.resourcePrimary {
|
|
947
|
+
min-height: 44px;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
@media (max-width: 280px) {
|
|
952
|
+
.resourceChildren {
|
|
953
|
+
margin-left: 8px;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.resourcePreview {
|
|
957
|
+
display: none;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
690
960
|
`,
|
|
691
961
|
];
|
|
692
962
|
|
|
@@ -694,6 +964,70 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
694
964
|
return new Map(this.sessions.map((session) => [session.id, session]));
|
|
695
965
|
}
|
|
696
966
|
|
|
967
|
+
private get resourcesById(): Map<string, IHarnessResourceMeta> {
|
|
968
|
+
const result = new Map<string, IHarnessResourceMeta>();
|
|
969
|
+
for (const resource of this.resources) {
|
|
970
|
+
if (!resource.id || !resource.kind || !resource.title || result.has(resource.id)) continue;
|
|
971
|
+
result.set(resource.id, resource);
|
|
972
|
+
}
|
|
973
|
+
return result;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** First valid record wins; unknown and duplicate records remain untouched but are not projected. */
|
|
977
|
+
private get acceptedResourceAssociations(): Map<string, string> {
|
|
978
|
+
const resourcesById = this.resourcesById;
|
|
979
|
+
const sessionsById = this.sessionsById;
|
|
980
|
+
const result = new Map<string, string>();
|
|
981
|
+
for (const association of this.resourceAssociations) {
|
|
982
|
+
if (
|
|
983
|
+
!resourcesById.has(association.resourceId)
|
|
984
|
+
|| !sessionsById.has(association.sessionId)
|
|
985
|
+
|| result.has(association.resourceId)
|
|
986
|
+
) continue;
|
|
987
|
+
result.set(association.resourceId, association.sessionId);
|
|
988
|
+
}
|
|
989
|
+
return result;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private get acceptedResources(): IHarnessResourceMeta[] {
|
|
993
|
+
return [...this.resourcesById.values()];
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private resourceMatchesSearch(resourceArg: IHarnessResourceMeta, queryArg: string): boolean {
|
|
997
|
+
return [resourceArg.id, resourceArg.kind, resourceArg.title, resourceArg.preview]
|
|
998
|
+
.filter(Boolean)
|
|
999
|
+
.join(' ')
|
|
1000
|
+
.toLowerCase()
|
|
1001
|
+
.includes(queryArg);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private sessionMatchesSearch(sessionArg: IHarnessSessionMeta, queryArg: string): boolean {
|
|
1005
|
+
return [sessionArg.title, sessionArg.preview, sessionArg.id]
|
|
1006
|
+
.filter(Boolean)
|
|
1007
|
+
.join(' ')
|
|
1008
|
+
.toLowerCase()
|
|
1009
|
+
.includes(queryArg);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private resourcesForSession(sessionArg: IHarnessSessionMeta): IHarnessResourceMeta[] {
|
|
1013
|
+
const associations = this.acceptedResourceAssociations;
|
|
1014
|
+
const associated = this.acceptedResources.filter(
|
|
1015
|
+
(resource) => associations.get(resource.id) === sessionArg.id,
|
|
1016
|
+
);
|
|
1017
|
+
const query = this.search.trim().toLowerCase();
|
|
1018
|
+
if (!query || this.sessionMatchesSearch(sessionArg, query)) return associated;
|
|
1019
|
+
return associated.filter((resource) => this.resourceMatchesSearch(resource, query));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
private get unattachedResources(): IHarnessResourceMeta[] {
|
|
1023
|
+
const associations = this.acceptedResourceAssociations;
|
|
1024
|
+
const query = this.search.trim().toLowerCase();
|
|
1025
|
+
return this.acceptedResources.filter((resource) => (
|
|
1026
|
+
!associations.has(resource.id)
|
|
1027
|
+
&& (!query || this.resourceMatchesSearch(resource, query))
|
|
1028
|
+
));
|
|
1029
|
+
}
|
|
1030
|
+
|
|
697
1031
|
private get groupedSessionIds(): Set<string> {
|
|
698
1032
|
const ids = new Set<string>();
|
|
699
1033
|
for (const group of this.groups) {
|
|
@@ -726,12 +1060,19 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
726
1060
|
|
|
727
1061
|
private get filteredSessions(): IHarnessSessionMeta[] {
|
|
728
1062
|
const query = this.search.trim().toLowerCase();
|
|
1063
|
+
const associations = this.acceptedResourceAssociations;
|
|
1064
|
+
const matchingAttachedSessionIds = new Set(
|
|
1065
|
+
this.acceptedResources
|
|
1066
|
+
.filter((resource) => query && this.resourceMatchesSearch(resource, query))
|
|
1067
|
+
.map((resource) => associations.get(resource.id))
|
|
1068
|
+
.filter((sessionId): sessionId is string => sessionId !== undefined),
|
|
1069
|
+
);
|
|
729
1070
|
const sorted = [...this.sessions].sort(
|
|
730
1071
|
(left, right) => (right.updatedAt ?? right.createdAt ?? 0) - (left.updatedAt ?? left.createdAt ?? 0),
|
|
731
1072
|
);
|
|
732
1073
|
if (!query) return sorted;
|
|
733
|
-
return sorted.filter(
|
|
734
|
-
|
|
1074
|
+
return sorted.filter(
|
|
1075
|
+
(session) => this.sessionMatchesSearch(session, query) || matchingAttachedSessionIds.has(session.id),
|
|
735
1076
|
);
|
|
736
1077
|
}
|
|
737
1078
|
|
|
@@ -739,6 +1080,61 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
739
1080
|
this.shadowRoot?.querySelector<HTMLInputElement>('.searchRow input')?.focus();
|
|
740
1081
|
}
|
|
741
1082
|
|
|
1083
|
+
public requestResourceAssociation(
|
|
1084
|
+
resourceIdArg: string,
|
|
1085
|
+
toSessionIdArg: string | null,
|
|
1086
|
+
sourceArg: THarnessResourceAssociationInteractionSource = 'action',
|
|
1087
|
+
): boolean {
|
|
1088
|
+
const resource = this.resourcesById.get(resourceIdArg);
|
|
1089
|
+
if (!resource) return false;
|
|
1090
|
+
const fromSessionId = this.acceptedResourceAssociations.get(resourceIdArg) ?? null;
|
|
1091
|
+
if (fromSessionId === toSessionIdArg) return false;
|
|
1092
|
+
if (
|
|
1093
|
+
toSessionIdArg !== null
|
|
1094
|
+
&& (!this.sessionsById.has(toSessionIdArg) || !this.isResourceTargetEligible(resourceIdArg, toSessionIdArg))
|
|
1095
|
+
) return false;
|
|
1096
|
+
if (toSessionIdArg === null && fromSessionId === null) return false;
|
|
1097
|
+
const detail: THarnessResourceAssociationRequestDetail = fromSessionId === null
|
|
1098
|
+
? {
|
|
1099
|
+
intent: 'attach',
|
|
1100
|
+
resource,
|
|
1101
|
+
fromSessionId: null,
|
|
1102
|
+
toSessionId: toSessionIdArg!,
|
|
1103
|
+
source: sourceArg,
|
|
1104
|
+
}
|
|
1105
|
+
: toSessionIdArg === null
|
|
1106
|
+
? {
|
|
1107
|
+
intent: 'detach',
|
|
1108
|
+
resource,
|
|
1109
|
+
fromSessionId,
|
|
1110
|
+
toSessionId: null,
|
|
1111
|
+
source: sourceArg,
|
|
1112
|
+
}
|
|
1113
|
+
: {
|
|
1114
|
+
intent: 'reassign',
|
|
1115
|
+
resource,
|
|
1116
|
+
fromSessionId,
|
|
1117
|
+
toSessionId: toSessionIdArg,
|
|
1118
|
+
source: sourceArg,
|
|
1119
|
+
};
|
|
1120
|
+
this.pendingResourceAssociationRequest = {
|
|
1121
|
+
resourceId: resource.id,
|
|
1122
|
+
resourceTitle: resource.title || resource.id,
|
|
1123
|
+
fromSessionId,
|
|
1124
|
+
toSessionId: toSessionIdArg,
|
|
1125
|
+
intent: detail.intent,
|
|
1126
|
+
};
|
|
1127
|
+
this.moveAnnouncement = this.resourceAssociationAnnouncement(detail, false);
|
|
1128
|
+
this.dispatchEvent(
|
|
1129
|
+
new CustomEvent('harness-resource-association-request', {
|
|
1130
|
+
detail,
|
|
1131
|
+
bubbles: true,
|
|
1132
|
+
composed: true,
|
|
1133
|
+
}),
|
|
1134
|
+
);
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
742
1138
|
public updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
743
1139
|
super.updated(changedProperties);
|
|
744
1140
|
this.observeScrollGeometry();
|
|
@@ -750,19 +1146,41 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
750
1146
|
) {
|
|
751
1147
|
this.finishPointerDrag(false);
|
|
752
1148
|
}
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
1149
|
+
if (changedProperties.has('sessions')) {
|
|
1150
|
+
const knownIds = new Set(this.sessions.map((session) => session.id));
|
|
1151
|
+
if (
|
|
1152
|
+
this.pointerDragGesture?.type === 'session'
|
|
1153
|
+
&& !knownIds.has(this.pointerDragGesture.sessionId)
|
|
1154
|
+
) {
|
|
1155
|
+
this.finishPointerDrag(false);
|
|
1156
|
+
}
|
|
1157
|
+
const nextExpanded = new Set(
|
|
1158
|
+
[...this.expandedSessionIds].filter((sessionId) => knownIds.has(sessionId)),
|
|
1159
|
+
);
|
|
1160
|
+
if (
|
|
1161
|
+
nextExpanded.size !== this.expandedSessionIds.size
|
|
1162
|
+
|| [...nextExpanded].some((sessionId) => !this.expandedSessionIds.has(sessionId))
|
|
1163
|
+
) {
|
|
1164
|
+
this.expandedSessionIds = nextExpanded;
|
|
1165
|
+
}
|
|
757
1166
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1167
|
+
if (changedProperties.has('resources') || changedProperties.has('resourceAssociations')) {
|
|
1168
|
+
const gesture = this.pointerDragGesture;
|
|
1169
|
+
if (gesture?.type === 'resource') {
|
|
1170
|
+
const currentSessionId = this.acceptedResourceAssociations.get(gesture.resourceId) ?? null;
|
|
1171
|
+
if (!this.resourcesById.has(gesture.resourceId) || currentSessionId !== gesture.fromSessionId) {
|
|
1172
|
+
this.finishPointerDrag(false);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (
|
|
1176
|
+
this.resourceContextResourceId
|
|
1177
|
+
&& !this.resourcesById.has(this.resourceContextResourceId)
|
|
1178
|
+
) this.closeResourceContext(this.resourceContextGeneration);
|
|
1179
|
+
this.reconcilePendingResourceAssociation();
|
|
1180
|
+
}
|
|
1181
|
+
if (changedProperties.has('eligibleSessionIdsByResourceId')) {
|
|
1182
|
+
const gesture = this.pointerDragGesture;
|
|
1183
|
+
if (gesture?.type === 'resource') this.finishPointerDrag(false);
|
|
766
1184
|
}
|
|
767
1185
|
}
|
|
768
1186
|
|
|
@@ -776,6 +1194,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
776
1194
|
|
|
777
1195
|
public async disconnectedCallback(): Promise<void> {
|
|
778
1196
|
this.finishPointerDrag(false);
|
|
1197
|
+
this.clearResourceContextState();
|
|
779
1198
|
this.scrollResizeObserver?.disconnect();
|
|
780
1199
|
this.scrollResizeObserver = undefined;
|
|
781
1200
|
this.observedScrollContainer = undefined;
|
|
@@ -789,7 +1208,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
789
1208
|
<div class="searchRow">
|
|
790
1209
|
<dees-icon class="searchIcon" icon="lucide:Search" iconSize="13"></dees-icon>
|
|
791
1210
|
<input
|
|
792
|
-
placeholder
|
|
1211
|
+
placeholder=${this.acceptedResources.length
|
|
1212
|
+
? 'Search conversations and resources'
|
|
1213
|
+
: 'Search conversations'}
|
|
1214
|
+
aria-label=${this.acceptedResources.length
|
|
1215
|
+
? 'Search conversations and resources'
|
|
1216
|
+
: 'Search conversations'}
|
|
793
1217
|
.value=${this.search}
|
|
794
1218
|
@input=${(event: Event) => {
|
|
795
1219
|
this.search = (event.currentTarget as HTMLInputElement).value;
|
|
@@ -839,9 +1263,16 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
839
1263
|
|
|
840
1264
|
private renderFlatList(): TemplateResult {
|
|
841
1265
|
const sessions = this.filteredSessions;
|
|
842
|
-
|
|
1266
|
+
const unattachedResources = this.unattachedResources;
|
|
1267
|
+
if (!sessions.length && !unattachedResources.length) {
|
|
843
1268
|
return html`<div class="emptyNote">
|
|
844
|
-
${this.
|
|
1269
|
+
${this.acceptedResources.length
|
|
1270
|
+
? this.sessions.length
|
|
1271
|
+
? 'No conversations or resources match this search.'
|
|
1272
|
+
: 'No resources match this search.'
|
|
1273
|
+
: this.sessions.length
|
|
1274
|
+
? 'No conversations match this search.'
|
|
1275
|
+
: this.emptyText}
|
|
845
1276
|
</div>`;
|
|
846
1277
|
}
|
|
847
1278
|
return html`
|
|
@@ -852,13 +1283,15 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
852
1283
|
(session) => this.renderItem(session, null, -1),
|
|
853
1284
|
)}
|
|
854
1285
|
</div>
|
|
1286
|
+
${this.renderUnattachedResources(unattachedResources)}
|
|
855
1287
|
`;
|
|
856
1288
|
}
|
|
857
1289
|
|
|
858
1290
|
private renderGroupedList(): TemplateResult {
|
|
859
1291
|
const byId = this.sessionsById;
|
|
860
1292
|
const ungrouped = this.ungroupedSessions;
|
|
861
|
-
|
|
1293
|
+
const unattachedResources = this.unattachedResources;
|
|
1294
|
+
if (!this.sessions.length && !this.groups.length && !unattachedResources.length) {
|
|
862
1295
|
return html`<div class="emptyNote">${this.emptyText}</div>`;
|
|
863
1296
|
}
|
|
864
1297
|
return html`
|
|
@@ -917,6 +1350,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
917
1350
|
`
|
|
918
1351
|
: ''}
|
|
919
1352
|
${this.renderSection(ungrouped, null)}
|
|
1353
|
+
${this.renderUnattachedResources(unattachedResources)}
|
|
920
1354
|
`;
|
|
921
1355
|
}
|
|
922
1356
|
|
|
@@ -1000,12 +1434,16 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1000
1434
|
const state = session.state ?? 'normal';
|
|
1001
1435
|
const detailsId = `session-details-${session.id}`;
|
|
1002
1436
|
const dropIndex = index < 0 ? 0 : index;
|
|
1437
|
+
const resources = this.resourcesForSession(session);
|
|
1438
|
+
const isResourceDropTarget = this.resourceDropTargetSessionId === session.id;
|
|
1003
1439
|
return html`
|
|
1004
1440
|
<article
|
|
1005
1441
|
class="item state-${state} ${expanded ? 'expanded' : ''} ${
|
|
1006
1442
|
session.id === this.selectedSessionId ? 'selected' : ''
|
|
1007
1443
|
} ${
|
|
1008
1444
|
this.draggingSessionId === session.id ? 'dragging' : ''
|
|
1445
|
+
} ${
|
|
1446
|
+
isResourceDropTarget ? 'resourceDropTarget' : ''
|
|
1009
1447
|
}"
|
|
1010
1448
|
data-session-id=${session.id}
|
|
1011
1449
|
data-drop-group-id=${groupId ?? ''}
|
|
@@ -1091,6 +1529,20 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1091
1529
|
<dees-icon icon="lucide:ChevronDown" iconSize="15"></dees-icon>
|
|
1092
1530
|
</button>
|
|
1093
1531
|
</div>
|
|
1532
|
+
${isResourceDropTarget
|
|
1533
|
+
? html`<div class="resourceDropLabel">Associate resource with ${session.title || session.id}</div>`
|
|
1534
|
+
: ''}
|
|
1535
|
+
${resources.length
|
|
1536
|
+
? html`
|
|
1537
|
+
<ul class="resourceChildren" aria-label=${`Resources for ${session.title || session.id}`}>
|
|
1538
|
+
${repeat(
|
|
1539
|
+
resources,
|
|
1540
|
+
(resource) => resource.id,
|
|
1541
|
+
(resource) => this.renderResource(resource, session.id),
|
|
1542
|
+
)}
|
|
1543
|
+
</ul>
|
|
1544
|
+
`
|
|
1545
|
+
: ''}
|
|
1094
1546
|
${expanded
|
|
1095
1547
|
? html`
|
|
1096
1548
|
<div class="itemDetails" id=${detailsId}>
|
|
@@ -1110,6 +1562,129 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1110
1562
|
`;
|
|
1111
1563
|
}
|
|
1112
1564
|
|
|
1565
|
+
private renderUnattachedResources(resourcesArg: IHarnessResourceMeta[]): TemplateResult {
|
|
1566
|
+
if (!resourcesArg.length) return html``;
|
|
1567
|
+
return html`
|
|
1568
|
+
<section class="resourceSection" aria-labelledby="unattached-resources-heading">
|
|
1569
|
+
<h2 class="resourceSectionTitle" id="unattached-resources-heading">Resources</h2>
|
|
1570
|
+
<ul class="resourceList" aria-label="Unattached resources">
|
|
1571
|
+
${repeat(
|
|
1572
|
+
resourcesArg,
|
|
1573
|
+
(resource) => resource.id,
|
|
1574
|
+
(resource) => this.renderResource(resource, null),
|
|
1575
|
+
)}
|
|
1576
|
+
</ul>
|
|
1577
|
+
</section>
|
|
1578
|
+
`;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
private renderResource(
|
|
1582
|
+
resourceArg: IHarnessResourceMeta,
|
|
1583
|
+
sessionIdArg: string | null,
|
|
1584
|
+
): TemplateResult {
|
|
1585
|
+
const selected = resourceArg.id === this.selectedResourceId;
|
|
1586
|
+
const dragging = resourceArg.id === this.draggingResourceId;
|
|
1587
|
+
const eligibleTargetCount = this.eligibleSessionIds(resourceArg.id).filter(
|
|
1588
|
+
(sessionId) => sessionId !== sessionIdArg,
|
|
1589
|
+
).length;
|
|
1590
|
+
const contextOpen = resourceArg.id === this.resourceContextResourceId;
|
|
1591
|
+
return html`
|
|
1592
|
+
<li
|
|
1593
|
+
class="resourceRow ${selected ? 'selected' : ''} ${dragging ? 'resourceDragging' : ''}"
|
|
1594
|
+
data-resource-id=${resourceArg.id}
|
|
1595
|
+
data-resource-session-id=${sessionIdArg ?? ''}
|
|
1596
|
+
@dblclick=${(event: MouseEvent) => {
|
|
1597
|
+
event.stopPropagation();
|
|
1598
|
+
if (this.eventTargetsResourceControl(event)) return;
|
|
1599
|
+
this.emitResource('harness-resource-open', resourceArg, sessionIdArg);
|
|
1600
|
+
}}
|
|
1601
|
+
@contextmenu=${(event: MouseEvent) => {
|
|
1602
|
+
event.preventDefault();
|
|
1603
|
+
event.stopPropagation();
|
|
1604
|
+
const focusTarget = (event.composedPath().find((target) => (
|
|
1605
|
+
target instanceof HTMLElement
|
|
1606
|
+
&& target.matches('button:not(:disabled)')
|
|
1607
|
+
)) as HTMLElement | undefined)
|
|
1608
|
+
?? (event.currentTarget as HTMLElement).querySelector<HTMLElement>('.resourcePrimary')
|
|
1609
|
+
?? event.currentTarget as HTMLElement;
|
|
1610
|
+
this.emitResourceContext(event, resourceArg, sessionIdArg, 'pointer', focusTarget);
|
|
1611
|
+
}}
|
|
1612
|
+
>
|
|
1613
|
+
<button
|
|
1614
|
+
class="resourceDragHandle"
|
|
1615
|
+
type="button"
|
|
1616
|
+
title=${eligibleTargetCount
|
|
1617
|
+
? 'Drag to associate with an eligible conversation'
|
|
1618
|
+
: 'No eligible conversations'}
|
|
1619
|
+
aria-label=${`Associate ${resourceArg.title || resourceArg.id} by dragging`}
|
|
1620
|
+
aria-keyshortcuts="ContextMenu Shift+F10"
|
|
1621
|
+
?disabled=${eligibleTargetCount === 0}
|
|
1622
|
+
@pointerdown=${(event: PointerEvent) => {
|
|
1623
|
+
this.handleResourcePointerDown(event, resourceArg, sessionIdArg);
|
|
1624
|
+
}}
|
|
1625
|
+
@pointermove=${this.handlePointerMove}
|
|
1626
|
+
@pointerup=${this.handlePointerUp}
|
|
1627
|
+
@pointercancel=${this.handlePointerCancel}
|
|
1628
|
+
@lostpointercapture=${this.handleLostPointerCapture}
|
|
1629
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1630
|
+
this.handleResourceContextKey(event, resourceArg, sessionIdArg);
|
|
1631
|
+
}}
|
|
1632
|
+
>
|
|
1633
|
+
<dees-icon icon="lucide:GripVertical" iconSize="13"></dees-icon>
|
|
1634
|
+
</button>
|
|
1635
|
+
<button
|
|
1636
|
+
class="resourcePrimary"
|
|
1637
|
+
type="button"
|
|
1638
|
+
aria-current=${selected ? 'true' : 'false'}
|
|
1639
|
+
@click=${() => this.emitResource('harness-resource-select', resourceArg, sessionIdArg)}
|
|
1640
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1641
|
+
if (event.key === 'Enter') {
|
|
1642
|
+
event.preventDefault();
|
|
1643
|
+
this.emitResource('harness-resource-open', resourceArg, sessionIdArg);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
this.handleResourceContextKey(event, resourceArg, sessionIdArg);
|
|
1647
|
+
}}
|
|
1648
|
+
>
|
|
1649
|
+
<dees-icon
|
|
1650
|
+
class="resourceIcon"
|
|
1651
|
+
icon=${resourceArg.icon || 'lucide:Box'}
|
|
1652
|
+
iconSize="14"
|
|
1653
|
+
></dees-icon>
|
|
1654
|
+
<span class="resourceCopy">
|
|
1655
|
+
<span class="resourceTitle">${resourceArg.title || resourceArg.id}</span>
|
|
1656
|
+
${resourceArg.preview ? html`<span class="resourcePreview">${resourceArg.preview}</span>` : ''}
|
|
1657
|
+
<span class="resourceMeta">
|
|
1658
|
+
${resourceArg.kind}${resourceArg.state ? ` · ${resourceArg.state}` : ''}${
|
|
1659
|
+
resourceArg.updatedAt || resourceArg.createdAt
|
|
1660
|
+
? ` · ${harnessFormatRelativeTime(resourceArg.updatedAt ?? resourceArg.createdAt)}`
|
|
1661
|
+
: ''
|
|
1662
|
+
}
|
|
1663
|
+
</span>
|
|
1664
|
+
</span>
|
|
1665
|
+
</button>
|
|
1666
|
+
<button
|
|
1667
|
+
class="resourceActions"
|
|
1668
|
+
type="button"
|
|
1669
|
+
title="Resource actions"
|
|
1670
|
+
aria-label=${`Actions for ${resourceArg.title || resourceArg.id}`}
|
|
1671
|
+
aria-haspopup="menu"
|
|
1672
|
+
aria-expanded=${contextOpen ? 'true' : 'false'}
|
|
1673
|
+
aria-keyshortcuts="ContextMenu Shift+F10"
|
|
1674
|
+
@click=${(event: MouseEvent) => {
|
|
1675
|
+
event.stopPropagation();
|
|
1676
|
+
this.emitResourceContext(event, resourceArg, sessionIdArg, 'action', event.currentTarget as HTMLElement);
|
|
1677
|
+
}}
|
|
1678
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1679
|
+
this.handleResourceContextKey(event, resourceArg, sessionIdArg);
|
|
1680
|
+
}}
|
|
1681
|
+
>
|
|
1682
|
+
<dees-icon icon="lucide:Ellipsis" iconSize="15"></dees-icon>
|
|
1683
|
+
</button>
|
|
1684
|
+
</li>
|
|
1685
|
+
`;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1113
1688
|
private eventTargetsExpandToggle(event: Event): boolean {
|
|
1114
1689
|
return event.composedPath().some(
|
|
1115
1690
|
(target) => target instanceof HTMLElement && target.classList.contains('expandToggle'),
|
|
@@ -1122,6 +1697,15 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1122
1697
|
);
|
|
1123
1698
|
}
|
|
1124
1699
|
|
|
1700
|
+
private eventTargetsResourceControl(event: Event): boolean {
|
|
1701
|
+
return event.composedPath().some(
|
|
1702
|
+
(target) => target instanceof HTMLElement && (
|
|
1703
|
+
target.classList.contains('resourceDragHandle')
|
|
1704
|
+
|| target.classList.contains('resourceActions')
|
|
1705
|
+
),
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1125
1709
|
private sessionStateLabel(state: NonNullable<IHarnessSessionMeta['state']>): string {
|
|
1126
1710
|
switch (state) {
|
|
1127
1711
|
case 'working': return 'Working';
|
|
@@ -1186,6 +1770,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1186
1770
|
if (!source) return;
|
|
1187
1771
|
const sourceRect = source.getBoundingClientRect();
|
|
1188
1772
|
this.pointerDragGesture = {
|
|
1773
|
+
type: 'session',
|
|
1189
1774
|
pointerId: eventArg.pointerId,
|
|
1190
1775
|
handle,
|
|
1191
1776
|
source,
|
|
@@ -1207,6 +1792,43 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1207
1792
|
}
|
|
1208
1793
|
}
|
|
1209
1794
|
|
|
1795
|
+
private handleResourcePointerDown(
|
|
1796
|
+
eventArg: PointerEvent,
|
|
1797
|
+
resourceArg: IHarnessResourceMeta,
|
|
1798
|
+
fromSessionIdArg: string | null,
|
|
1799
|
+
): void {
|
|
1800
|
+
if (
|
|
1801
|
+
this.pointerDragGesture
|
|
1802
|
+
|| eventArg.button !== 0
|
|
1803
|
+
|| !eventArg.isPrimary
|
|
1804
|
+
|| this.eligibleSessionIds(resourceArg.id).every((sessionId) => sessionId === fromSessionIdArg)
|
|
1805
|
+
) return;
|
|
1806
|
+
const handle = eventArg.currentTarget as HTMLElement;
|
|
1807
|
+
const source = handle.closest<HTMLElement>('.resourceRow');
|
|
1808
|
+
if (!source) return;
|
|
1809
|
+
const sourceRect = source.getBoundingClientRect();
|
|
1810
|
+
this.pointerDragGesture = {
|
|
1811
|
+
type: 'resource',
|
|
1812
|
+
pointerId: eventArg.pointerId,
|
|
1813
|
+
handle,
|
|
1814
|
+
source,
|
|
1815
|
+
resourceId: resourceArg.id,
|
|
1816
|
+
fromSessionId: fromSessionIdArg,
|
|
1817
|
+
startClientX: eventArg.clientX,
|
|
1818
|
+
startClientY: eventArg.clientY,
|
|
1819
|
+
grabOffsetX: eventArg.clientX - sourceRect.left,
|
|
1820
|
+
grabOffsetY: eventArg.clientY - sourceRect.top,
|
|
1821
|
+
phase: 'pending',
|
|
1822
|
+
};
|
|
1823
|
+
window.addEventListener('keydown', this.handleDragKeyDown, true);
|
|
1824
|
+
window.addEventListener('blur', this.handleDragWindowBlur);
|
|
1825
|
+
try {
|
|
1826
|
+
handle.setPointerCapture(eventArg.pointerId);
|
|
1827
|
+
} catch {
|
|
1828
|
+
this.finishPointerDrag(false);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1210
1832
|
private readonly handlePointerMove = (eventArg: PointerEvent): void => {
|
|
1211
1833
|
const gesture = this.pointerDragGesture;
|
|
1212
1834
|
if (!gesture || eventArg.pointerId !== gesture.pointerId) return;
|
|
@@ -1221,8 +1843,21 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1221
1843
|
eventArg.clientY - gesture.startClientY,
|
|
1222
1844
|
) < dragThresholdPixels
|
|
1223
1845
|
) return;
|
|
1224
|
-
if (
|
|
1846
|
+
if (
|
|
1847
|
+
gesture.phase === 'pending'
|
|
1848
|
+
&& !(gesture.type === 'session'
|
|
1849
|
+
? this.startPointerDrag(eventArg, gesture)
|
|
1850
|
+
: this.startResourcePointerDrag(eventArg, gesture))
|
|
1851
|
+
) return;
|
|
1225
1852
|
eventArg.preventDefault();
|
|
1853
|
+
if (gesture.type === 'resource') {
|
|
1854
|
+
this.moveResourceDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1855
|
+
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1856
|
+
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1857
|
+
this.updateDragAutoScrollVelocity();
|
|
1858
|
+
this.updateResourceDropTargetFromPointer(eventArg.clientX, eventArg.clientY);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1226
1861
|
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1227
1862
|
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1228
1863
|
this.dragAutoScrollClientY = eventArg.clientY;
|
|
@@ -1234,6 +1869,14 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1234
1869
|
const gesture = this.pointerDragGesture;
|
|
1235
1870
|
if (!gesture || eventArg.pointerId !== gesture.pointerId) return;
|
|
1236
1871
|
if (gesture.phase === 'active') {
|
|
1872
|
+
if (gesture.type === 'resource') {
|
|
1873
|
+
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1874
|
+
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1875
|
+
this.moveResourceDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1876
|
+
this.updateResourceDropTargetFromPointer(eventArg.clientX, eventArg.clientY);
|
|
1877
|
+
this.finishPointerDrag(true);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1237
1880
|
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1238
1881
|
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1239
1882
|
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
@@ -1289,6 +1932,27 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1289
1932
|
return true;
|
|
1290
1933
|
}
|
|
1291
1934
|
|
|
1935
|
+
private startResourcePointerDrag(
|
|
1936
|
+
eventArg: PointerEvent,
|
|
1937
|
+
gestureArg: IResourcePointerDragGesture,
|
|
1938
|
+
): boolean {
|
|
1939
|
+
const sourceRect = gestureArg.source.getBoundingClientRect();
|
|
1940
|
+
gestureArg.source.classList.add('resourceDragging');
|
|
1941
|
+
gestureArg.source.style.setProperty('--resource-drag-preview-width', `${sourceRect.width}px`);
|
|
1942
|
+
this.moveResourceDragPreview(eventArg.clientX, eventArg.clientY, gestureArg);
|
|
1943
|
+
gestureArg.source.setAttribute('popover', 'manual');
|
|
1944
|
+
try {
|
|
1945
|
+
gestureArg.source.showPopover();
|
|
1946
|
+
} catch {
|
|
1947
|
+
this.finishPointerDrag(false);
|
|
1948
|
+
return false;
|
|
1949
|
+
}
|
|
1950
|
+
gestureArg.phase = 'active';
|
|
1951
|
+
this.draggingResourceId = gestureArg.resourceId;
|
|
1952
|
+
this.resourceDropTargetSessionId = '';
|
|
1953
|
+
return true;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1292
1956
|
private moveDragPreview(
|
|
1293
1957
|
clientXArg: number,
|
|
1294
1958
|
clientYArg: number,
|
|
@@ -1304,6 +1968,34 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1304
1968
|
);
|
|
1305
1969
|
}
|
|
1306
1970
|
|
|
1971
|
+
private moveResourceDragPreview(
|
|
1972
|
+
clientXArg: number,
|
|
1973
|
+
clientYArg: number,
|
|
1974
|
+
gestureArg: IResourcePointerDragGesture,
|
|
1975
|
+
): void {
|
|
1976
|
+
gestureArg.source.style.setProperty(
|
|
1977
|
+
'--resource-drag-preview-x',
|
|
1978
|
+
`${clientXArg - gestureArg.grabOffsetX}px`,
|
|
1979
|
+
);
|
|
1980
|
+
gestureArg.source.style.setProperty(
|
|
1981
|
+
'--resource-drag-preview-y',
|
|
1982
|
+
`${clientYArg - gestureArg.grabOffsetY}px`,
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
private updateResourceDropTargetFromPointer(clientXArg: number, clientYArg: number): void {
|
|
1987
|
+
const gesture = this.pointerDragGesture;
|
|
1988
|
+
if (gesture?.type !== 'resource' || gesture.phase !== 'active') return;
|
|
1989
|
+
const hit = this.shadowRoot?.elementFromPoint(clientXArg, clientYArg);
|
|
1990
|
+
const target = hit?.closest<HTMLElement>('.item[data-session-id]');
|
|
1991
|
+
const sessionId = target?.dataset.sessionId ?? '';
|
|
1992
|
+
this.resourceDropTargetSessionId = (
|
|
1993
|
+
sessionId
|
|
1994
|
+
&& sessionId !== gesture.fromSessionId
|
|
1995
|
+
&& this.isResourceTargetEligible(gesture.resourceId, sessionId)
|
|
1996
|
+
) ? sessionId : '';
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1307
1999
|
private setDropSlot(groupIdArg: string | null, indexArg: number): void {
|
|
1308
2000
|
const current = this.dropSlot;
|
|
1309
2001
|
if (current && current.groupId === groupIdArg && current.index === indexArg) return;
|
|
@@ -1323,7 +2015,8 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1323
2015
|
|
|
1324
2016
|
private updateDragAutoScrollVelocity(): void {
|
|
1325
2017
|
const container = this.scrollContainer;
|
|
1326
|
-
|
|
2018
|
+
const gesture = this.pointerDragGesture;
|
|
2019
|
+
if (!container || gesture?.phase !== 'active') return;
|
|
1327
2020
|
const rect = container.getBoundingClientRect();
|
|
1328
2021
|
if (this.dragAutoScrollClientX < rect.left || this.dragAutoScrollClientX > rect.right) {
|
|
1329
2022
|
this.stopDragAutoScroll();
|
|
@@ -1356,7 +2049,8 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1356
2049
|
private readonly runDragAutoScroll = (timestampArg: number): void => {
|
|
1357
2050
|
this.dragAutoScrollFrame = undefined;
|
|
1358
2051
|
const container = this.scrollContainer;
|
|
1359
|
-
|
|
2052
|
+
const gesture = this.pointerDragGesture;
|
|
2053
|
+
if (!container || gesture?.phase !== 'active' || this.dragAutoScrollVelocity === 0) return;
|
|
1360
2054
|
const elapsedMs = Math.min(34, Math.max(0, timestampArg - this.dragAutoScrollLastFrame));
|
|
1361
2055
|
this.dragAutoScrollLastFrame = timestampArg;
|
|
1362
2056
|
const previousScrollTop = container.scrollTop;
|
|
@@ -1369,7 +2063,14 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1369
2063
|
this.stopDragAutoScroll();
|
|
1370
2064
|
return;
|
|
1371
2065
|
}
|
|
1372
|
-
|
|
2066
|
+
if (gesture.type === 'resource') {
|
|
2067
|
+
this.updateResourceDropTargetFromPointer(
|
|
2068
|
+
this.dragAutoScrollClientX,
|
|
2069
|
+
this.dragAutoScrollClientY,
|
|
2070
|
+
);
|
|
2071
|
+
} else {
|
|
2072
|
+
this.updateDropSlotFromPointer();
|
|
2073
|
+
}
|
|
1373
2074
|
this.dragAutoScrollFrame = requestAnimationFrame(this.runDragAutoScroll);
|
|
1374
2075
|
};
|
|
1375
2076
|
|
|
@@ -1585,6 +2286,171 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1585
2286
|
}
|
|
1586
2287
|
}
|
|
1587
2288
|
|
|
2289
|
+
private eligibleSessionIds(resourceIdArg: string): string[] {
|
|
2290
|
+
const knownSessionIds = new Set(this.sessions.map((session) => session.id));
|
|
2291
|
+
const result: string[] = [];
|
|
2292
|
+
const seen = new Set<string>();
|
|
2293
|
+
for (const sessionId of this.eligibleSessionIdsByResourceId[resourceIdArg] ?? []) {
|
|
2294
|
+
if (!knownSessionIds.has(sessionId) || seen.has(sessionId)) continue;
|
|
2295
|
+
seen.add(sessionId);
|
|
2296
|
+
result.push(sessionId);
|
|
2297
|
+
}
|
|
2298
|
+
return result;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
private isResourceTargetEligible(resourceIdArg: string, sessionIdArg: string): boolean {
|
|
2302
|
+
return this.eligibleSessionIds(resourceIdArg).includes(sessionIdArg);
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
private emitResource(
|
|
2306
|
+
nameArg: string,
|
|
2307
|
+
resourceArg: IHarnessResourceMeta,
|
|
2308
|
+
sessionIdArg: string | null,
|
|
2309
|
+
): void {
|
|
2310
|
+
const detail: IHarnessResourceEventDetail = {
|
|
2311
|
+
resource: resourceArg,
|
|
2312
|
+
sessionId: sessionIdArg,
|
|
2313
|
+
};
|
|
2314
|
+
this.dispatchEvent(new CustomEvent(nameArg, { detail, bubbles: true, composed: true }));
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
private handleResourceContextKey(
|
|
2318
|
+
eventArg: KeyboardEvent,
|
|
2319
|
+
resourceArg: IHarnessResourceMeta,
|
|
2320
|
+
sessionIdArg: string | null,
|
|
2321
|
+
): void {
|
|
2322
|
+
if (!(eventArg.key === 'ContextMenu' || (eventArg.key === 'F10' && eventArg.shiftKey))) return;
|
|
2323
|
+
eventArg.preventDefault();
|
|
2324
|
+
eventArg.stopPropagation();
|
|
2325
|
+
this.emitResourceContext(
|
|
2326
|
+
eventArg,
|
|
2327
|
+
resourceArg,
|
|
2328
|
+
sessionIdArg,
|
|
2329
|
+
'keyboard',
|
|
2330
|
+
eventArg.currentTarget as HTMLElement,
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
private emitResourceContext(
|
|
2335
|
+
eventArg: MouseEvent | KeyboardEvent,
|
|
2336
|
+
resourceArg: IHarnessResourceMeta,
|
|
2337
|
+
sessionIdArg: string | null,
|
|
2338
|
+
sourceArg: THarnessResourceContextSource,
|
|
2339
|
+
focusTargetArg: HTMLElement,
|
|
2340
|
+
): void {
|
|
2341
|
+
const rect = focusTargetArg.getBoundingClientRect();
|
|
2342
|
+
const contextGeneration = ++this.resourceContextGeneration;
|
|
2343
|
+
this.resourceContextResourceId = resourceArg.id;
|
|
2344
|
+
this.resourceContextFocusTarget = focusTargetArg;
|
|
2345
|
+
this.resourceContextFocusClass = [
|
|
2346
|
+
'resourceActions',
|
|
2347
|
+
'resourcePrimary',
|
|
2348
|
+
'resourceDragHandle',
|
|
2349
|
+
].find((className) => focusTargetArg.classList.contains(className)) ?? 'resourcePrimary';
|
|
2350
|
+
const detail: IHarnessResourceContextDetail = {
|
|
2351
|
+
resource: resourceArg,
|
|
2352
|
+
sessionId: sessionIdArg,
|
|
2353
|
+
clientX: sourceArg === 'pointer' && eventArg instanceof MouseEvent
|
|
2354
|
+
? eventArg.clientX
|
|
2355
|
+
: rect.left + rect.width / 2,
|
|
2356
|
+
clientY: sourceArg === 'pointer' && eventArg instanceof MouseEvent
|
|
2357
|
+
? eventArg.clientY
|
|
2358
|
+
: rect.bottom,
|
|
2359
|
+
source: sourceArg,
|
|
2360
|
+
originalEvent: eventArg,
|
|
2361
|
+
requestAssociation: (toSessionIdArg) => this.requestResourceAssociation(
|
|
2362
|
+
resourceArg.id,
|
|
2363
|
+
toSessionIdArg,
|
|
2364
|
+
sourceArg === 'keyboard' ? 'keyboard' : 'action',
|
|
2365
|
+
),
|
|
2366
|
+
close: () => this.closeResourceContext(contextGeneration),
|
|
2367
|
+
};
|
|
2368
|
+
this.dispatchEvent(
|
|
2369
|
+
new CustomEvent('harness-resource-context', { detail, bubbles: true, composed: true }),
|
|
2370
|
+
);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
private closeResourceContext(contextGenerationArg: number): void {
|
|
2374
|
+
if (
|
|
2375
|
+
!this.isConnected
|
|
2376
|
+
|| contextGenerationArg !== this.resourceContextGeneration
|
|
2377
|
+
) return;
|
|
2378
|
+
const focusTarget = this.resourceContextFocusTarget;
|
|
2379
|
+
const resourceId = this.resourceContextResourceId;
|
|
2380
|
+
const focusClass = this.resourceContextFocusClass;
|
|
2381
|
+
this.clearResourceContextState();
|
|
2382
|
+
const closeGeneration = this.resourceContextGeneration;
|
|
2383
|
+
void this.updateComplete.then(() => {
|
|
2384
|
+
if (!this.isConnected || closeGeneration !== this.resourceContextGeneration) return;
|
|
2385
|
+
if (focusTarget?.isConnected) {
|
|
2386
|
+
focusTarget.focus();
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
2389
|
+
const row = Array.from(
|
|
2390
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.resourceRow[data-resource-id]') ?? [],
|
|
2391
|
+
).find((candidate) => candidate.dataset.resourceId === resourceId);
|
|
2392
|
+
row?.querySelector<HTMLElement>(`.${focusClass || 'resourcePrimary'}`)?.focus();
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
private clearResourceContextState(): void {
|
|
2397
|
+
this.resourceContextGeneration += 1;
|
|
2398
|
+
this.resourceContextResourceId = '';
|
|
2399
|
+
this.resourceContextFocusTarget = undefined;
|
|
2400
|
+
this.resourceContextFocusClass = '';
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2403
|
+
private reconcilePendingResourceAssociation(): void {
|
|
2404
|
+
const pending = this.pendingResourceAssociationRequest;
|
|
2405
|
+
if (!pending) return;
|
|
2406
|
+
if (!this.resourcesById.has(pending.resourceId)) {
|
|
2407
|
+
this.pendingResourceAssociationRequest = undefined;
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
const currentSessionId = this.acceptedResourceAssociations.get(pending.resourceId) ?? null;
|
|
2411
|
+
if (currentSessionId !== pending.toSessionId) {
|
|
2412
|
+
if (currentSessionId !== pending.fromSessionId) {
|
|
2413
|
+
const controlledSession = currentSessionId
|
|
2414
|
+
? this.sessionsById.get(currentSessionId)?.title ?? currentSessionId
|
|
2415
|
+
: 'unattached';
|
|
2416
|
+
this.moveAnnouncement = `Association request for ${pending.resourceTitle} was not applied; controlled association is ${controlledSession}.`;
|
|
2417
|
+
this.pendingResourceAssociationRequest = undefined;
|
|
2418
|
+
}
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
const fromSession = pending.fromSessionId
|
|
2422
|
+
? this.sessionsById.get(pending.fromSessionId)?.title ?? pending.fromSessionId
|
|
2423
|
+
: '';
|
|
2424
|
+
const toSession = pending.toSessionId
|
|
2425
|
+
? this.sessionsById.get(pending.toSessionId)?.title ?? pending.toSessionId
|
|
2426
|
+
: '';
|
|
2427
|
+
this.moveAnnouncement = pending.intent === 'attach'
|
|
2428
|
+
? `${pending.resourceTitle} is now attached to ${toSession}.`
|
|
2429
|
+
: pending.intent === 'detach'
|
|
2430
|
+
? `${pending.resourceTitle} is now detached from ${fromSession}.`
|
|
2431
|
+
: `${pending.resourceTitle} is now reassigned from ${fromSession} to ${toSession}.`;
|
|
2432
|
+
this.pendingResourceAssociationRequest = undefined;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
private resourceAssociationAnnouncement(
|
|
2436
|
+
detailArg: THarnessResourceAssociationRequestDetail,
|
|
2437
|
+
completedArg: boolean,
|
|
2438
|
+
): string {
|
|
2439
|
+
const resourceTitle = detailArg.resource.title || detailArg.resource.id;
|
|
2440
|
+
const fromTitle = detailArg.fromSessionId
|
|
2441
|
+
? this.sessionsById.get(detailArg.fromSessionId)?.title ?? detailArg.fromSessionId
|
|
2442
|
+
: '';
|
|
2443
|
+
const toTitle = detailArg.toSessionId
|
|
2444
|
+
? this.sessionsById.get(detailArg.toSessionId)?.title ?? detailArg.toSessionId
|
|
2445
|
+
: '';
|
|
2446
|
+
const verb = completedArg ? 'completed' : 'requested';
|
|
2447
|
+
return detailArg.intent === 'attach'
|
|
2448
|
+
? `Attach ${verb} for ${resourceTitle} to ${toTitle}.`
|
|
2449
|
+
: detailArg.intent === 'detach'
|
|
2450
|
+
? `Detach ${verb} for ${resourceTitle} from ${fromTitle}.`
|
|
2451
|
+
: `Reassign ${verb} for ${resourceTitle} from ${fromTitle} to ${toTitle}.`;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
1588
2454
|
private async animateCardReflow(
|
|
1589
2455
|
previousRectsArg: Map<string, DOMRect>,
|
|
1590
2456
|
generationArg: number,
|
|
@@ -1626,9 +2492,25 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1626
2492
|
private finishPointerDrag(commitArg: boolean): void {
|
|
1627
2493
|
const gesture = this.pointerDragGesture;
|
|
1628
2494
|
if (!gesture) return;
|
|
1629
|
-
const
|
|
2495
|
+
const sessionMoveDetail = (
|
|
2496
|
+
gesture.type === 'session'
|
|
2497
|
+
&& commitArg
|
|
2498
|
+
&& gesture.phase === 'active'
|
|
2499
|
+
&& this.dropSlot
|
|
2500
|
+
)
|
|
1630
2501
|
? this.createMoveDetail(gesture.sessionId, this.dropSlot.groupId, this.dropSlot.index)
|
|
1631
2502
|
: undefined;
|
|
2503
|
+
const resourceRequest = (
|
|
2504
|
+
gesture.type === 'resource'
|
|
2505
|
+
&& commitArg
|
|
2506
|
+
&& gesture.phase === 'active'
|
|
2507
|
+
&& this.resourceDropTargetSessionId
|
|
2508
|
+
)
|
|
2509
|
+
? {
|
|
2510
|
+
resourceId: gesture.resourceId,
|
|
2511
|
+
toSessionId: this.resourceDropTargetSessionId,
|
|
2512
|
+
}
|
|
2513
|
+
: undefined;
|
|
1632
2514
|
this.pointerDragGesture = undefined;
|
|
1633
2515
|
window.removeEventListener('keydown', this.handleDragKeyDown, true);
|
|
1634
2516
|
window.removeEventListener('blur', this.handleDragWindowBlur);
|
|
@@ -1637,14 +2519,19 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1637
2519
|
this.cancelReflowAnimations();
|
|
1638
2520
|
if (gesture.source.matches(':popover-open')) gesture.source.hidePopover();
|
|
1639
2521
|
gesture.source.removeAttribute('popover');
|
|
1640
|
-
gesture.source.classList.remove('dragging');
|
|
2522
|
+
gesture.source.classList.remove('dragging', 'resourceDragging');
|
|
1641
2523
|
for (const property of [
|
|
1642
2524
|
'--drag-preview-width',
|
|
1643
2525
|
'--drag-preview-height',
|
|
1644
2526
|
'--drag-preview-x',
|
|
1645
2527
|
'--drag-preview-y',
|
|
2528
|
+
'--resource-drag-preview-width',
|
|
2529
|
+
'--resource-drag-preview-x',
|
|
2530
|
+
'--resource-drag-preview-y',
|
|
1646
2531
|
]) gesture.source.style.removeProperty(property);
|
|
1647
2532
|
this.draggingSessionId = '';
|
|
2533
|
+
this.draggingResourceId = '';
|
|
2534
|
+
this.resourceDropTargetSessionId = '';
|
|
1648
2535
|
this.draggedItemHeight = 0;
|
|
1649
2536
|
this.dropSlot = undefined;
|
|
1650
2537
|
try {
|
|
@@ -1654,11 +2541,18 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1654
2541
|
} catch {
|
|
1655
2542
|
// Capture may already be released or the handle may have disconnected.
|
|
1656
2543
|
}
|
|
1657
|
-
if (
|
|
2544
|
+
if (sessionMoveDetail) {
|
|
1658
2545
|
this.dispatchEvent(
|
|
1659
|
-
new CustomEvent('harness-session-move', {
|
|
2546
|
+
new CustomEvent('harness-session-move', {
|
|
2547
|
+
detail: sessionMoveDetail,
|
|
2548
|
+
bubbles: true,
|
|
2549
|
+
composed: true,
|
|
2550
|
+
}),
|
|
1660
2551
|
);
|
|
1661
2552
|
}
|
|
2553
|
+
if (resourceRequest) {
|
|
2554
|
+
this.requestResourceAssociation(resourceRequest.resourceId, resourceRequest.toSessionId, 'drop');
|
|
2555
|
+
}
|
|
1662
2556
|
}
|
|
1663
2557
|
|
|
1664
2558
|
private emit(name: string, session: IHarnessSessionMeta): void {
|