@design.estate/dees-catalog 11.2.2 → 12.0.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 +1861 -1290
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.d.ts +62 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.demo.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.demo.js +38 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.js +455 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/index.js +2 -0
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +39 -10
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +81 -27
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +327 -115
- package/dist_ts_web/elements/00group-harness/index.d.ts +1 -0
- package/dist_ts_web/elements/00group-harness/index.js +2 -1
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +35 -16
- package/package.json +1 -1
- package/readme.md +54 -22
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.demo.ts +46 -0
- package/ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.ts +420 -0
- package/ts_web/elements/00group-harness/dees-harness-conversation-picker/index.ts +1 -0
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +94 -36
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +357 -120
- package/ts_web/elements/00group-harness/index.ts +1 -0
- package/ts_web/elements/00group-harness/interfaces.ts +41 -16
package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts
CHANGED
|
@@ -27,7 +27,8 @@ import type {
|
|
|
27
27
|
IHarnessSessionMeta,
|
|
28
28
|
THarnessItemMoveSource,
|
|
29
29
|
THarnessResourceAssociationInteractionSource,
|
|
30
|
-
|
|
30
|
+
THarnessResourceAssociateDetail,
|
|
31
|
+
THarnessResourceAssociateMode,
|
|
31
32
|
THarnessResourceContextSource,
|
|
32
33
|
} from '../interfaces.js';
|
|
33
34
|
import { harnessFormatRelativeTime } from '../harness.helpers.js';
|
|
@@ -36,6 +37,12 @@ import '../../00group-utility/dees-icon/dees-icon.js';
|
|
|
36
37
|
const { repeat } = directives;
|
|
37
38
|
const dragAutoScrollMaxPixelsPerSecond = 900;
|
|
38
39
|
const dragThresholdPixels = 4;
|
|
40
|
+
/**
|
|
41
|
+
* Share of a row's height, centred, that attaches instead of inserting: the centre half of the
|
|
42
|
+
* row attaches and the outer quarters reposition, so reordering a resource past a conversation
|
|
43
|
+
* never turns into an attachment.
|
|
44
|
+
*/
|
|
45
|
+
const attachCentreBandRatio = 0.5;
|
|
39
46
|
|
|
40
47
|
declare global {
|
|
41
48
|
interface HTMLElementTagNameMap {
|
|
@@ -98,7 +105,9 @@ interface IPendingResourceAssociationRequest {
|
|
|
98
105
|
resourceTitle: string;
|
|
99
106
|
fromTarget: IHarnessSessionListItemRef | null;
|
|
100
107
|
toTarget: IHarnessSessionListItemRef | null;
|
|
101
|
-
|
|
108
|
+
mode: THarnessResourceAssociateMode;
|
|
109
|
+
/** The target set the request would produce, compared against the controlled set on update. */
|
|
110
|
+
expectedTargets: IHarnessSessionListItemRef[];
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
interface IKeyboardMoveFocus {
|
|
@@ -118,10 +127,13 @@ type TSectionRenderEntry =
|
|
|
118
127
|
*
|
|
119
128
|
* Sessions and host resources are peers in ONE ordered list: a resource row
|
|
120
129
|
* can be dragged anywhere a session can, including between sessions and into
|
|
121
|
-
* or out of groups.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
130
|
+
* or out of groups. A resource can also be attached to several items at once.
|
|
131
|
+
* Attaching happens either through `requestResourceAssociation()` from a host
|
|
132
|
+
* menu, or by releasing a resource drag on the centre half of an eligible row;
|
|
133
|
+
* the outer quarters of that row, and every gap between rows, stay purely
|
|
134
|
+
* positional. Attaching never moves a row, and the association renders only as
|
|
135
|
+
* markers on the resource row. Every change is requested through
|
|
136
|
+
* `harness-resource-associate`; the host owns the controlled set.
|
|
125
137
|
*
|
|
126
138
|
* The component never mutates its inputs: reordering emits `harness-item-move`,
|
|
127
139
|
* the "new group" button emits `harness-group-create-request`, and
|
|
@@ -170,8 +182,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
170
182
|
accessor selectedResourceId = '';
|
|
171
183
|
|
|
172
184
|
/**
|
|
173
|
-
* Serializable
|
|
174
|
-
*
|
|
185
|
+
* Serializable association targets, honoured by host menus and by the drop
|
|
186
|
+
* affordance alike: an ineligible row offers no attachment and commits
|
|
187
|
+
* nothing. A missing entry leaves the resource unrestricted; an empty array
|
|
188
|
+
* means no eligible target.
|
|
175
189
|
*/
|
|
176
190
|
@property({ attribute: false })
|
|
177
191
|
accessor eligibleTargetsByResourceId: Record<string, IHarnessSessionListItemRef[]> = {};
|
|
@@ -185,6 +199,13 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
185
199
|
@state()
|
|
186
200
|
accessor dropSlot: IDropSlot | undefined = undefined;
|
|
187
201
|
|
|
202
|
+
/**
|
|
203
|
+
* The row a resource drag would attach to. Mutually exclusive with `dropSlot`: a pointer is
|
|
204
|
+
* either inserting between rows or attaching to one, never both.
|
|
205
|
+
*/
|
|
206
|
+
@state()
|
|
207
|
+
accessor dropAttachTarget: IHarnessSessionListItemRef | undefined = undefined;
|
|
208
|
+
|
|
188
209
|
@state()
|
|
189
210
|
accessor draggedItemHeight = 0;
|
|
190
211
|
|
|
@@ -699,8 +720,8 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
699
720
|
white-space: nowrap;
|
|
700
721
|
}
|
|
701
722
|
|
|
702
|
-
/* Decorative
|
|
703
|
-
.
|
|
723
|
+
/* Decorative markers for the controlled associations; never interactive. */
|
|
724
|
+
.resourceAttachments {
|
|
704
725
|
display: flex;
|
|
705
726
|
min-width: 0;
|
|
706
727
|
align-items: center;
|
|
@@ -711,10 +732,49 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
711
732
|
font-weight: 600;
|
|
712
733
|
}
|
|
713
734
|
|
|
714
|
-
.resourceAttachment
|
|
735
|
+
.resourceAttachment {
|
|
736
|
+
display: flex;
|
|
737
|
+
min-width: 0;
|
|
738
|
+
align-items: center;
|
|
739
|
+
gap: 2px;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/* Past two names the row keeps one line and the title carries the full list. */
|
|
743
|
+
.resourceAttachmentOverflow {
|
|
744
|
+
flex: 0 0 auto;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.resourceAttachments dees-icon {
|
|
715
748
|
flex: 0 0 auto;
|
|
716
749
|
}
|
|
717
750
|
|
|
751
|
+
/*
|
|
752
|
+
* A drop on the centre of a row attaches to it. The ring is the whole affordance: no gap
|
|
753
|
+
* opens, because nothing is being inserted.
|
|
754
|
+
*/
|
|
755
|
+
.item.attachTarget,
|
|
756
|
+
.resourceRow.attachTarget {
|
|
757
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
758
|
+
outline-offset: -1px;
|
|
759
|
+
background: var(--dees-color-nav-selected);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/* Absolutely positioned so appearing mid-drag cannot reflow the row under the pointer. */
|
|
763
|
+
.attachBadge {
|
|
764
|
+
position: absolute;
|
|
765
|
+
top: 50%;
|
|
766
|
+
right: 10px;
|
|
767
|
+
display: grid;
|
|
768
|
+
place-items: center;
|
|
769
|
+
width: 20px;
|
|
770
|
+
height: 20px;
|
|
771
|
+
transform: translateY(-50%);
|
|
772
|
+
border-radius: var(--dees-radius-full);
|
|
773
|
+
background: var(--dees-color-accent-primary);
|
|
774
|
+
color: var(--dees-color-bg-primary);
|
|
775
|
+
pointer-events: none;
|
|
776
|
+
}
|
|
777
|
+
|
|
718
778
|
.resourceAttachmentName {
|
|
719
779
|
min-width: 0;
|
|
720
780
|
overflow: hidden;
|
|
@@ -957,23 +1017,32 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
957
1017
|
* First valid record wins; unknown records, self-references and duplicates
|
|
958
1018
|
* remain untouched but are not projected. Keyed by resource id.
|
|
959
1019
|
*/
|
|
960
|
-
private get acceptedResourceAssociations(): Map<string, IHarnessSessionListItemRef> {
|
|
1020
|
+
private get acceptedResourceAssociations(): Map<string, IHarnessSessionListItemRef[]> {
|
|
961
1021
|
const resourcesById = this.resourcesById;
|
|
962
1022
|
const itemsByKey = this.itemsByKey;
|
|
963
|
-
const result = new Map<string, IHarnessSessionListItemRef>();
|
|
1023
|
+
const result = new Map<string, IHarnessSessionListItemRef[]>();
|
|
964
1024
|
for (const association of this.resourceAssociations) {
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1025
|
+
if (!association || !resourcesById.has(association.resourceId)) continue;
|
|
1026
|
+
const targets = result.get(association.resourceId) ?? [];
|
|
1027
|
+
for (const target of association.targets ?? []) {
|
|
1028
|
+
if (
|
|
1029
|
+
!target
|
|
1030
|
+
|| !itemsByKey.has(itemKey(target))
|
|
1031
|
+
|| sameItem(target, { kind: 'resource', id: association.resourceId })
|
|
1032
|
+
|| targets.some((candidate) => sameItem(candidate, target))
|
|
1033
|
+
) continue;
|
|
1034
|
+
targets.push(itemRef(target));
|
|
1035
|
+
}
|
|
1036
|
+
result.set(association.resourceId, targets);
|
|
973
1037
|
}
|
|
974
1038
|
return result;
|
|
975
1039
|
}
|
|
976
1040
|
|
|
1041
|
+
/** The controlled attachments of one resource, in declaration order; empty when unattached. */
|
|
1042
|
+
private resourceTargets(resourceIdArg: string): IHarnessSessionListItemRef[] {
|
|
1043
|
+
return this.acceptedResourceAssociations.get(resourceIdArg) ?? [];
|
|
1044
|
+
}
|
|
1045
|
+
|
|
977
1046
|
private get acceptedResources(): IHarnessResourceMeta[] {
|
|
978
1047
|
return [...this.resourcesById.values()];
|
|
979
1048
|
}
|
|
@@ -1128,58 +1197,91 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1128
1197
|
this.shadowRoot?.querySelector<HTMLInputElement>('.searchRow input')?.focus();
|
|
1129
1198
|
}
|
|
1130
1199
|
|
|
1200
|
+
/**
|
|
1201
|
+
* Requests one association change. With no explicit mode the list infers it from the controlled
|
|
1202
|
+
* set: an unattached resource attaches, a singly attached one moves, and a resource with several
|
|
1203
|
+
* attachments is refused — only the host knows which of them the user meant, so it must pass a
|
|
1204
|
+
* mode (and a `fromTarget` for `move` and `detach`).
|
|
1205
|
+
*/
|
|
1131
1206
|
public requestResourceAssociation(
|
|
1132
1207
|
resourceIdArg: string,
|
|
1133
1208
|
toTargetArg: IHarnessSessionListItemRef | null,
|
|
1134
1209
|
sourceArg: THarnessResourceAssociationInteractionSource = 'action',
|
|
1210
|
+
modeArg?: THarnessResourceAssociateMode,
|
|
1211
|
+
fromTargetArg?: IHarnessSessionListItemRef | null,
|
|
1135
1212
|
): boolean {
|
|
1136
1213
|
const resource = this.resourcesById.get(resourceIdArg);
|
|
1137
1214
|
if (!resource) return false;
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1215
|
+
const currentTargets = this.resourceTargets(resourceIdArg);
|
|
1216
|
+
const toTarget = toTargetArg === null || toTargetArg === undefined ? null : itemRef(toTargetArg);
|
|
1140
1217
|
if (
|
|
1141
|
-
|
|
1218
|
+
toTarget !== null
|
|
1142
1219
|
&& (
|
|
1143
|
-
!this.itemsByKey.has(itemKey(
|
|
1144
|
-
|| sameItem(
|
|
1145
|
-
|| !this.isResourceTargetEligible(resourceIdArg,
|
|
1220
|
+
!this.itemsByKey.has(itemKey(toTarget))
|
|
1221
|
+
|| sameItem(toTarget, { kind: 'resource', id: resourceIdArg })
|
|
1222
|
+
|| !this.isResourceTargetEligible(resourceIdArg, toTarget)
|
|
1146
1223
|
)
|
|
1147
1224
|
) return false;
|
|
1148
|
-
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1225
|
+
|
|
1226
|
+
const mode = modeArg ?? (
|
|
1227
|
+
toTarget === null
|
|
1228
|
+
? 'detach'
|
|
1229
|
+
: currentTargets.length === 0
|
|
1230
|
+
? 'attach'
|
|
1231
|
+
: currentTargets.length === 1
|
|
1232
|
+
? 'move'
|
|
1233
|
+
: undefined
|
|
1234
|
+
);
|
|
1235
|
+
// An ambiguous request is refused rather than guessed at.
|
|
1236
|
+
if (!mode) return false;
|
|
1237
|
+
|
|
1238
|
+
const explicitFrom = fromTargetArg === null || fromTargetArg === undefined
|
|
1239
|
+
? undefined
|
|
1240
|
+
: itemRef(fromTargetArg);
|
|
1241
|
+
const fromTarget = explicitFrom
|
|
1242
|
+
?? (currentTargets.length === 1 ? currentTargets[0]! : undefined);
|
|
1243
|
+
|
|
1244
|
+
if (mode === 'attach' || mode === 'attach-additional') {
|
|
1245
|
+
if (toTarget === null) return false;
|
|
1246
|
+
if (currentTargets.some((candidate) => sameItem(candidate, toTarget))) return false;
|
|
1247
|
+
if (mode === 'attach' && currentTargets.length > 0) return false;
|
|
1248
|
+
if (mode === 'attach-additional' && currentTargets.length === 0) return false;
|
|
1249
|
+
}
|
|
1250
|
+
if (mode === 'move' || mode === 'detach') {
|
|
1251
|
+
if (!fromTarget) return false;
|
|
1252
|
+
if (!currentTargets.some((candidate) => sameItem(candidate, fromTarget))) return false;
|
|
1253
|
+
if (mode === 'move') {
|
|
1254
|
+
if (toTarget === null || sameItem(fromTarget, toTarget)) return false;
|
|
1255
|
+
if (currentTargets.some((candidate) => sameItem(candidate, toTarget))) return false;
|
|
1256
|
+
}
|
|
1257
|
+
if (mode === 'detach' && toTarget !== null) return false;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const detail = {
|
|
1261
|
+
mode,
|
|
1262
|
+
resource,
|
|
1263
|
+
fromTarget: mode === 'attach' || mode === 'attach-additional' ? null : fromTarget!,
|
|
1264
|
+
toTarget: mode === 'detach' ? null : toTarget!,
|
|
1265
|
+
source: sourceArg,
|
|
1266
|
+
} as THarnessResourceAssociateDetail;
|
|
1267
|
+
|
|
1268
|
+
const expectedTargets = mode === 'attach' || mode === 'attach-additional'
|
|
1269
|
+
? [...currentTargets, toTarget!]
|
|
1270
|
+
: mode === 'detach'
|
|
1271
|
+
? currentTargets.filter((candidate) => !sameItem(candidate, fromTarget!))
|
|
1272
|
+
: currentTargets.map((candidate) => (sameItem(candidate, fromTarget!) ? toTarget! : candidate));
|
|
1273
|
+
|
|
1173
1274
|
this.pendingResourceAssociationRequest = {
|
|
1174
1275
|
resourceId: resource.id,
|
|
1175
1276
|
resourceTitle: resource.title || resource.id,
|
|
1176
|
-
fromTarget,
|
|
1177
|
-
toTarget,
|
|
1178
|
-
|
|
1277
|
+
fromTarget: detail.fromTarget,
|
|
1278
|
+
toTarget: detail.toTarget,
|
|
1279
|
+
mode,
|
|
1280
|
+
expectedTargets,
|
|
1179
1281
|
};
|
|
1180
1282
|
this.moveAnnouncement = this.resourceAssociationAnnouncement(detail, false);
|
|
1181
1283
|
this.dispatchEvent(
|
|
1182
|
-
new CustomEvent('harness-resource-
|
|
1284
|
+
new CustomEvent('harness-resource-associate', {
|
|
1183
1285
|
detail,
|
|
1184
1286
|
bubbles: true,
|
|
1185
1287
|
composed: true,
|
|
@@ -1439,7 +1541,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1439
1541
|
entry.item.resource,
|
|
1440
1542
|
groupIdArg,
|
|
1441
1543
|
entry.logicalIndex,
|
|
1442
|
-
associations.get(entry.item.id) ??
|
|
1544
|
+
associations.get(entry.item.id) ?? [],
|
|
1443
1545
|
),
|
|
1444
1546
|
)}
|
|
1445
1547
|
${this.draggingItem && sortableArg && !hasPlaceholder && this.sortableItems(itemsArg).length === 0
|
|
@@ -1519,6 +1621,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1519
1621
|
session.id === this.selectedSessionId ? 'selected' : ''
|
|
1520
1622
|
} ${
|
|
1521
1623
|
sameItem(this.draggingItem, { kind: 'session', id: session.id }) ? 'dragging' : ''
|
|
1624
|
+
} ${
|
|
1625
|
+
sameItem(this.dropAttachTarget ?? null, { kind: 'session', id: session.id })
|
|
1626
|
+
? 'attachTarget'
|
|
1627
|
+
: ''
|
|
1522
1628
|
}"
|
|
1523
1629
|
data-session-id=${session.id}
|
|
1524
1630
|
data-item-kind="session"
|
|
@@ -1526,6 +1632,9 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1526
1632
|
data-drop-group-id=${groupId ?? ''}
|
|
1527
1633
|
data-drop-index=${dropIndex}
|
|
1528
1634
|
data-drop-kind=${index < 0 ? 'slot' : 'card'}
|
|
1635
|
+
data-attach-target=${
|
|
1636
|
+
sameItem(this.dropAttachTarget ?? null, { kind: 'session', id: session.id }) ? 'true' : ''
|
|
1637
|
+
}
|
|
1529
1638
|
@dblclick=${(event: MouseEvent) => {
|
|
1530
1639
|
if (this.eventTargetsExpandToggle(event) || this.eventTargetsDragHandle(event)
|
|
1531
1640
|
|| this.eventTargetsSessionActions(event)) return;
|
|
@@ -1546,6 +1655,9 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1546
1655
|
);
|
|
1547
1656
|
}}
|
|
1548
1657
|
>
|
|
1658
|
+
${sameItem(this.dropAttachTarget ?? null, { kind: 'session', id: session.id })
|
|
1659
|
+
? html`<span class="attachBadge" aria-hidden="true"><dees-icon icon="lucide:Link" iconSize="12"></dees-icon></span>`
|
|
1660
|
+
: ''}
|
|
1549
1661
|
<div class="itemInner">
|
|
1550
1662
|
<div class="itemHeader ${draggable ? 'draggable' : ''}">
|
|
1551
1663
|
${draggable
|
|
@@ -1634,13 +1746,17 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1634
1746
|
resourceArg: IHarnessResourceMeta,
|
|
1635
1747
|
groupIdArg: string | null,
|
|
1636
1748
|
indexArg: number,
|
|
1637
|
-
|
|
1749
|
+
attachedTargets: IHarnessSessionListItemRef[],
|
|
1638
1750
|
): TemplateResult {
|
|
1639
1751
|
const selected = resourceArg.id === this.selectedResourceId;
|
|
1640
1752
|
const draggable = indexArg >= 0;
|
|
1641
1753
|
const dragging = sameItem(this.draggingItem, { kind: 'resource', id: resourceArg.id });
|
|
1642
1754
|
const contextOpen = resourceArg.id === this.resourceContextResourceId;
|
|
1643
|
-
const
|
|
1755
|
+
const attachedTitles = attachedTargets.map((target) => this.itemTitleFor(target));
|
|
1756
|
+
// Two names read at a glance; beyond that a count keeps the row one line, with every name
|
|
1757
|
+
// still reachable through the title.
|
|
1758
|
+
const visibleAttachments = attachedTargets.slice(0, 2);
|
|
1759
|
+
const overflowAttachments = attachedTargets.length - visibleAttachments.length;
|
|
1644
1760
|
const title = resourceArg.title || resourceArg.id;
|
|
1645
1761
|
const projectLabel = resourceArg.projectLabel?.trim() ?? '';
|
|
1646
1762
|
const dropIndex = indexArg < 0 ? 0 : indexArg;
|
|
@@ -1648,18 +1764,28 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1648
1764
|
<article
|
|
1649
1765
|
class="resourceRow ${selected ? 'selected' : ''} ${draggable ? 'draggable' : ''} ${
|
|
1650
1766
|
dragging ? 'dragging' : ''
|
|
1767
|
+
} ${
|
|
1768
|
+
sameItem(this.dropAttachTarget ?? null, { kind: 'resource', id: resourceArg.id })
|
|
1769
|
+
? 'attachTarget'
|
|
1770
|
+
: ''
|
|
1651
1771
|
}"
|
|
1652
1772
|
data-resource-id=${resourceArg.id}
|
|
1653
1773
|
data-item-kind="resource"
|
|
1654
1774
|
data-item-id=${resourceArg.id}
|
|
1655
|
-
data-resource-target=${
|
|
1775
|
+
data-resource-target=${attachedTargets.map((target) => `${target.kind}:${target.id}`).join(' ')}
|
|
1776
|
+
data-resource-target-count=${attachedTargets.length}
|
|
1656
1777
|
data-drop-group-id=${groupIdArg ?? ''}
|
|
1657
1778
|
data-drop-index=${dropIndex}
|
|
1658
1779
|
data-drop-kind=${indexArg < 0 ? 'slot' : 'card'}
|
|
1780
|
+
data-attach-target=${
|
|
1781
|
+
sameItem(this.dropAttachTarget ?? null, { kind: 'resource', id: resourceArg.id })
|
|
1782
|
+
? 'true'
|
|
1783
|
+
: ''
|
|
1784
|
+
}
|
|
1659
1785
|
@dblclick=${(event: MouseEvent) => {
|
|
1660
1786
|
event.stopPropagation();
|
|
1661
1787
|
if (this.eventTargetsResourceControl(event) || this.eventTargetsDragHandle(event)) return;
|
|
1662
|
-
this.emitResource('harness-resource-open', resourceArg,
|
|
1788
|
+
this.emitResource('harness-resource-open', resourceArg, attachedTargets);
|
|
1663
1789
|
}}
|
|
1664
1790
|
@contextmenu=${(event: MouseEvent) => {
|
|
1665
1791
|
event.preventDefault();
|
|
@@ -1670,9 +1796,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1670
1796
|
)) as HTMLElement | undefined)
|
|
1671
1797
|
?? (event.currentTarget as HTMLElement).querySelector<HTMLElement>('.resourcePrimary')
|
|
1672
1798
|
?? event.currentTarget as HTMLElement;
|
|
1673
|
-
this.emitResourceContext(event, resourceArg,
|
|
1799
|
+
this.emitResourceContext(event, resourceArg, attachedTargets, 'pointer', focusTarget);
|
|
1674
1800
|
}}
|
|
1675
1801
|
>
|
|
1802
|
+
${sameItem(this.dropAttachTarget ?? null, { kind: 'resource', id: resourceArg.id })
|
|
1803
|
+
? html`<span class="attachBadge" aria-hidden="true"><dees-icon icon="lucide:Link" iconSize="12"></dees-icon></span>`
|
|
1804
|
+
: ''}
|
|
1676
1805
|
${draggable
|
|
1677
1806
|
? this.renderDragHandle(
|
|
1678
1807
|
{ kind: 'resource', id: resourceArg.id },
|
|
@@ -1685,14 +1814,14 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1685
1814
|
class="resourcePrimary"
|
|
1686
1815
|
type="button"
|
|
1687
1816
|
aria-current=${selected ? 'true' : 'false'}
|
|
1688
|
-
@click=${() => this.emitResource('harness-resource-select', resourceArg,
|
|
1817
|
+
@click=${() => this.emitResource('harness-resource-select', resourceArg, attachedTargets)}
|
|
1689
1818
|
@keydown=${(event: KeyboardEvent) => {
|
|
1690
1819
|
if (event.key === 'Enter') {
|
|
1691
1820
|
event.preventDefault();
|
|
1692
|
-
this.emitResource('harness-resource-open', resourceArg,
|
|
1821
|
+
this.emitResource('harness-resource-open', resourceArg, attachedTargets);
|
|
1693
1822
|
return;
|
|
1694
1823
|
}
|
|
1695
|
-
this.handleResourceContextKey(event, resourceArg,
|
|
1824
|
+
this.handleResourceContextKey(event, resourceArg, attachedTargets);
|
|
1696
1825
|
}}
|
|
1697
1826
|
>
|
|
1698
1827
|
<dees-icon
|
|
@@ -1712,12 +1841,26 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1712
1841
|
: ''
|
|
1713
1842
|
}
|
|
1714
1843
|
</span>
|
|
1715
|
-
${
|
|
1844
|
+
${attachedTargets.length
|
|
1716
1845
|
? html`
|
|
1717
|
-
<span class="
|
|
1846
|
+
<span class="resourceAttachments" title=${`Attached to ${attachedTitles.join(', ')}`}>
|
|
1718
1847
|
<dees-icon aria-hidden="true" icon="lucide:Link" iconSize="10"></dees-icon>
|
|
1719
1848
|
<span class="visuallyHidden">Attached to </span>
|
|
1720
|
-
|
|
1849
|
+
${repeat(
|
|
1850
|
+
visibleAttachments,
|
|
1851
|
+
(target) => itemKey(target),
|
|
1852
|
+
(target, index) => html`
|
|
1853
|
+
<span class="resourceAttachment">
|
|
1854
|
+
<span class="resourceAttachmentName">${this.itemTitleFor(target)}</span>
|
|
1855
|
+
${index < visibleAttachments.length - 1 || overflowAttachments > 0
|
|
1856
|
+
? html`<span aria-hidden="true">,</span>`
|
|
1857
|
+
: ''}
|
|
1858
|
+
</span>
|
|
1859
|
+
`,
|
|
1860
|
+
)}
|
|
1861
|
+
${overflowAttachments > 0
|
|
1862
|
+
? html`<span class="resourceAttachmentOverflow">+${overflowAttachments}</span>`
|
|
1863
|
+
: ''}
|
|
1721
1864
|
</span>
|
|
1722
1865
|
`
|
|
1723
1866
|
: ''}
|
|
@@ -1736,13 +1879,13 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1736
1879
|
this.emitResourceContext(
|
|
1737
1880
|
event,
|
|
1738
1881
|
resourceArg,
|
|
1739
|
-
|
|
1882
|
+
attachedTargets,
|
|
1740
1883
|
'action',
|
|
1741
1884
|
event.currentTarget as HTMLElement,
|
|
1742
1885
|
);
|
|
1743
1886
|
}}
|
|
1744
1887
|
@keydown=${(event: KeyboardEvent) => {
|
|
1745
|
-
this.handleResourceContextKey(event, resourceArg,
|
|
1888
|
+
this.handleResourceContextKey(event, resourceArg, attachedTargets);
|
|
1746
1889
|
}}
|
|
1747
1890
|
>
|
|
1748
1891
|
<dees-icon icon="lucide:Ellipsis" iconSize="15"></dees-icon>
|
|
@@ -1971,14 +2114,31 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1971
2114
|
|
|
1972
2115
|
private setDropSlot(groupIdArg: string | null, indexArg: number): void {
|
|
1973
2116
|
const current = this.dropSlot;
|
|
1974
|
-
if (
|
|
2117
|
+
if (
|
|
2118
|
+
current
|
|
2119
|
+
&& current.groupId === groupIdArg
|
|
2120
|
+
&& current.index === indexArg
|
|
2121
|
+
&& !this.dropAttachTarget
|
|
2122
|
+
) return;
|
|
2123
|
+
this.dropAttachTarget = undefined;
|
|
1975
2124
|
const previousRects = this.captureRowRects();
|
|
1976
2125
|
const generation = ++this.reflowGeneration;
|
|
1977
2126
|
this.dropSlot = { groupId: groupIdArg, index: indexArg };
|
|
1978
2127
|
void this.animateRowReflow(previousRects, generation);
|
|
1979
2128
|
}
|
|
1980
2129
|
|
|
2130
|
+
private setDropAttachTarget(targetArg: IHarnessSessionListItemRef): void {
|
|
2131
|
+
if (sameItem(this.dropAttachTarget ?? null, targetArg) && !this.dropSlot) return;
|
|
2132
|
+
const previousRects = this.captureRowRects();
|
|
2133
|
+
const generation = ++this.reflowGeneration;
|
|
2134
|
+
this.dropAttachTarget = itemRef(targetArg);
|
|
2135
|
+
// An attaching pointer closes any open insertion gap, so the list stops offering both.
|
|
2136
|
+
this.dropSlot = undefined;
|
|
2137
|
+
void this.animateRowReflow(previousRects, generation);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
1981
2140
|
private clearDropSlot(): void {
|
|
2141
|
+
this.dropAttachTarget = undefined;
|
|
1982
2142
|
if (!this.dropSlot) return;
|
|
1983
2143
|
const previousRects = this.captureRowRects();
|
|
1984
2144
|
const generation = ++this.reflowGeneration;
|
|
@@ -2090,12 +2250,46 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2090
2250
|
const baseIndex = Number(target.dataset.dropIndex);
|
|
2091
2251
|
const groupId = target.dataset.dropGroupId || null;
|
|
2092
2252
|
const rect = target.getBoundingClientRect();
|
|
2253
|
+
const attachTarget = this.attachTargetForDrop(target, pointerY, rect);
|
|
2254
|
+
if (attachTarget) {
|
|
2255
|
+
this.setDropAttachTarget(attachTarget);
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2093
2258
|
const index = target.dataset.dropKind === 'card' && pointerY >= rect.top + rect.height / 2
|
|
2094
2259
|
? baseIndex + 1
|
|
2095
2260
|
: baseIndex;
|
|
2096
2261
|
this.setDropSlot(groupId, index);
|
|
2097
2262
|
}
|
|
2098
2263
|
|
|
2264
|
+
/**
|
|
2265
|
+
* A resource released on the centre of an eligible row attaches to it. Only a row qualifies —
|
|
2266
|
+
* never an insertion slot — and only while a resource is the dragged item. A resource that is
|
|
2267
|
+
* already attached to several items is excluded: a drag cannot name which attachment would
|
|
2268
|
+
* move, so such a row offers no affordance and the drop stays purely positional. Moving one of
|
|
2269
|
+
* several attachments is a menu action.
|
|
2270
|
+
*/
|
|
2271
|
+
private attachTargetForDrop(
|
|
2272
|
+
targetArg: HTMLElement,
|
|
2273
|
+
pointerYArg: number,
|
|
2274
|
+
rectArg: DOMRect,
|
|
2275
|
+
): IHarnessSessionListItemRef | undefined {
|
|
2276
|
+
const dragging = this.draggingItem;
|
|
2277
|
+
if (!dragging || dragging.kind !== 'resource') return undefined;
|
|
2278
|
+
if (this.resourceTargets(dragging.id).length > 1) return undefined;
|
|
2279
|
+
if (targetArg.dataset.dropKind !== 'card') return undefined;
|
|
2280
|
+
const kind = targetArg.dataset.itemKind;
|
|
2281
|
+
const id = targetArg.dataset.itemId;
|
|
2282
|
+
if ((kind !== 'session' && kind !== 'resource') || !id) return undefined;
|
|
2283
|
+
const candidate: IHarnessSessionListItemRef = { kind, id };
|
|
2284
|
+
if (sameItem(candidate, dragging)) return undefined;
|
|
2285
|
+
if (!this.isResourceTargetEligible(dragging.id, candidate)) return undefined;
|
|
2286
|
+
// The ratio is the whole band, so each side of the centre gets half of it.
|
|
2287
|
+
const halfBand = (rectArg.height * attachCentreBandRatio) / 2;
|
|
2288
|
+
const centre = rectArg.top + rectArg.height / 2;
|
|
2289
|
+
if (Math.abs(pointerYArg - centre) > halfBand) return undefined;
|
|
2290
|
+
return candidate;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2099
2293
|
private get scrollContainer(): HTMLElement | undefined {
|
|
2100
2294
|
return this.shadowRoot?.querySelector<HTMLElement>('.list') ?? undefined;
|
|
2101
2295
|
}
|
|
@@ -2282,6 +2476,69 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2282
2476
|
return eligible === undefined || eligible.some((candidate) => sameItem(candidate, targetArg));
|
|
2283
2477
|
}
|
|
2284
2478
|
|
|
2479
|
+
private reconcilePendingResourceAssociation(): void {
|
|
2480
|
+
const pending = this.pendingResourceAssociationRequest;
|
|
2481
|
+
if (!pending) return;
|
|
2482
|
+
if (!this.resourcesById.has(pending.resourceId)) {
|
|
2483
|
+
this.pendingResourceAssociationRequest = undefined;
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
const current = this.resourceTargets(pending.resourceId);
|
|
2487
|
+
const sameSet = (leftArg: IHarnessSessionListItemRef[], rightArg: IHarnessSessionListItemRef[]): boolean =>
|
|
2488
|
+
leftArg.length === rightArg.length
|
|
2489
|
+
&& leftArg.every((candidate) => rightArg.some((other) => sameItem(candidate, other)));
|
|
2490
|
+
if (!sameSet(current, pending.expectedTargets)) {
|
|
2491
|
+
// Still the pre-request set: the host has not answered yet, so keep waiting.
|
|
2492
|
+
if (sameSet(current, this.pendingRequestPriorTargets(pending))) return;
|
|
2493
|
+
const controlledTitle = current.length
|
|
2494
|
+
? current.map((target) => this.itemTitleFor(target)).join(', ')
|
|
2495
|
+
: 'unattached';
|
|
2496
|
+
this.moveAnnouncement = `Association request for ${pending.resourceTitle} was not applied; controlled attachments are ${controlledTitle}.`;
|
|
2497
|
+
this.pendingResourceAssociationRequest = undefined;
|
|
2498
|
+
return;
|
|
2499
|
+
}
|
|
2500
|
+
const fromTitle = pending.fromTarget ? this.itemTitleFor(pending.fromTarget) : '';
|
|
2501
|
+
const toTitle = pending.toTarget ? this.itemTitleFor(pending.toTarget) : '';
|
|
2502
|
+
this.moveAnnouncement = pending.mode === 'attach' || pending.mode === 'attach-additional'
|
|
2503
|
+
? `${pending.resourceTitle} is now attached to ${toTitle}.`
|
|
2504
|
+
: pending.mode === 'detach'
|
|
2505
|
+
? `${pending.resourceTitle} is now detached from ${fromTitle}.`
|
|
2506
|
+
: `${pending.resourceTitle} is now moved from ${fromTitle} to ${toTitle}.`;
|
|
2507
|
+
this.pendingResourceAssociationRequest = undefined;
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
/** The set the request started from, reconstructed from the expected set and the request. */
|
|
2511
|
+
private pendingRequestPriorTargets(
|
|
2512
|
+
pendingArg: IPendingResourceAssociationRequest,
|
|
2513
|
+
): IHarnessSessionListItemRef[] {
|
|
2514
|
+
if (pendingArg.mode === 'attach' || pendingArg.mode === 'attach-additional') {
|
|
2515
|
+
return pendingArg.expectedTargets.filter((candidate) => !sameItem(candidate, pendingArg.toTarget));
|
|
2516
|
+
}
|
|
2517
|
+
if (pendingArg.mode === 'detach') {
|
|
2518
|
+
return [...pendingArg.expectedTargets, pendingArg.fromTarget!];
|
|
2519
|
+
}
|
|
2520
|
+
return pendingArg.expectedTargets.map(
|
|
2521
|
+
(candidate) => (sameItem(candidate, pendingArg.toTarget) ? pendingArg.fromTarget! : candidate),
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
private resourceAssociationAnnouncement(
|
|
2526
|
+
detailArg: THarnessResourceAssociateDetail,
|
|
2527
|
+
completedArg: boolean,
|
|
2528
|
+
): string {
|
|
2529
|
+
const resourceTitle = detailArg.resource.title || detailArg.resource.id;
|
|
2530
|
+
const fromTitle = detailArg.fromTarget ? this.itemTitleFor(detailArg.fromTarget) : '';
|
|
2531
|
+
const toTitle = detailArg.toTarget ? this.itemTitleFor(detailArg.toTarget) : '';
|
|
2532
|
+
const verb = completedArg ? 'completed' : 'requested';
|
|
2533
|
+
return detailArg.mode === 'attach'
|
|
2534
|
+
? `Attach ${verb} for ${resourceTitle} to ${toTitle}.`
|
|
2535
|
+
: detailArg.mode === 'attach-additional'
|
|
2536
|
+
? `Additional attach ${verb} for ${resourceTitle} to ${toTitle}.`
|
|
2537
|
+
: detailArg.mode === 'detach'
|
|
2538
|
+
? `Detach ${verb} for ${resourceTitle} from ${fromTitle}.`
|
|
2539
|
+
: `Move ${verb} for ${resourceTitle} from ${fromTitle} to ${toTitle}.`;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2285
2542
|
private itemTitleFor(refArg: IHarnessSessionListItemRef): string {
|
|
2286
2543
|
return this.itemsByKey.get(itemKey(refArg))?.title ?? refArg.id;
|
|
2287
2544
|
}
|
|
@@ -2294,11 +2551,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2294
2551
|
private emitResource(
|
|
2295
2552
|
nameArg: string,
|
|
2296
2553
|
resourceArg: IHarnessResourceMeta,
|
|
2297
|
-
|
|
2554
|
+
targetsArg: IHarnessSessionListItemRef[],
|
|
2298
2555
|
): void {
|
|
2299
2556
|
const detail: IHarnessResourceEventDetail = {
|
|
2300
2557
|
resource: resourceArg,
|
|
2301
|
-
|
|
2558
|
+
targets: targetsArg,
|
|
2302
2559
|
};
|
|
2303
2560
|
this.dispatchEvent(new CustomEvent(nameArg, { detail, bubbles: true, composed: true }));
|
|
2304
2561
|
}
|
|
@@ -2306,7 +2563,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2306
2563
|
private handleResourceContextKey(
|
|
2307
2564
|
eventArg: KeyboardEvent,
|
|
2308
2565
|
resourceArg: IHarnessResourceMeta,
|
|
2309
|
-
|
|
2566
|
+
targetsArg: IHarnessSessionListItemRef[],
|
|
2310
2567
|
): void {
|
|
2311
2568
|
if (!(eventArg.key === 'ContextMenu' || (eventArg.key === 'F10' && eventArg.shiftKey))) return;
|
|
2312
2569
|
eventArg.preventDefault();
|
|
@@ -2314,7 +2571,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2314
2571
|
this.emitResourceContext(
|
|
2315
2572
|
eventArg,
|
|
2316
2573
|
resourceArg,
|
|
2317
|
-
|
|
2574
|
+
targetsArg,
|
|
2318
2575
|
'keyboard',
|
|
2319
2576
|
eventArg.currentTarget as HTMLElement,
|
|
2320
2577
|
);
|
|
@@ -2323,7 +2580,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2323
2580
|
private emitResourceContext(
|
|
2324
2581
|
eventArg: MouseEvent | KeyboardEvent,
|
|
2325
2582
|
resourceArg: IHarnessResourceMeta,
|
|
2326
|
-
|
|
2583
|
+
targetsArg: IHarnessSessionListItemRef[],
|
|
2327
2584
|
sourceArg: THarnessResourceContextSource,
|
|
2328
2585
|
focusTargetArg: HTMLElement,
|
|
2329
2586
|
): void {
|
|
@@ -2338,7 +2595,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2338
2595
|
].find((className) => focusTargetArg.classList.contains(className)) ?? 'resourcePrimary';
|
|
2339
2596
|
const detail: IHarnessResourceContextDetail = {
|
|
2340
2597
|
resource: resourceArg,
|
|
2341
|
-
|
|
2598
|
+
targets: targetsArg,
|
|
2342
2599
|
clientX: sourceArg === 'pointer' && eventArg instanceof MouseEvent
|
|
2343
2600
|
? eventArg.clientX
|
|
2344
2601
|
: rect.left + rect.width / 2,
|
|
@@ -2347,10 +2604,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2347
2604
|
: rect.bottom,
|
|
2348
2605
|
source: sourceArg,
|
|
2349
2606
|
originalEvent: eventArg,
|
|
2350
|
-
requestAssociation: (toTargetArg) => this.requestResourceAssociation(
|
|
2607
|
+
requestAssociation: (toTargetArg, modeArg, fromTargetArg) => this.requestResourceAssociation(
|
|
2351
2608
|
resourceArg.id,
|
|
2352
2609
|
toTargetArg,
|
|
2353
2610
|
sourceArg === 'keyboard' ? 'keyboard' : 'action',
|
|
2611
|
+
modeArg,
|
|
2612
|
+
fromTargetArg,
|
|
2354
2613
|
),
|
|
2355
2614
|
close: () => this.closeResourceContext(contextGeneration),
|
|
2356
2615
|
};
|
|
@@ -2387,47 +2646,6 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2387
2646
|
this.resourceContextFocusClass = '';
|
|
2388
2647
|
}
|
|
2389
2648
|
|
|
2390
|
-
private reconcilePendingResourceAssociation(): void {
|
|
2391
|
-
const pending = this.pendingResourceAssociationRequest;
|
|
2392
|
-
if (!pending) return;
|
|
2393
|
-
if (!this.resourcesById.has(pending.resourceId)) {
|
|
2394
|
-
this.pendingResourceAssociationRequest = undefined;
|
|
2395
|
-
return;
|
|
2396
|
-
}
|
|
2397
|
-
const currentTarget = this.acceptedResourceAssociations.get(pending.resourceId) ?? null;
|
|
2398
|
-
if (!sameItem(currentTarget, pending.toTarget) && !(currentTarget === null && pending.toTarget === null)) {
|
|
2399
|
-
if (!sameItem(currentTarget, pending.fromTarget) && !(currentTarget === null && pending.fromTarget === null)) {
|
|
2400
|
-
const controlledTitle = currentTarget ? this.itemTitleFor(currentTarget) : 'unattached';
|
|
2401
|
-
this.moveAnnouncement = `Association request for ${pending.resourceTitle} was not applied; controlled association is ${controlledTitle}.`;
|
|
2402
|
-
this.pendingResourceAssociationRequest = undefined;
|
|
2403
|
-
}
|
|
2404
|
-
return;
|
|
2405
|
-
}
|
|
2406
|
-
const fromTitle = pending.fromTarget ? this.itemTitleFor(pending.fromTarget) : '';
|
|
2407
|
-
const toTitle = pending.toTarget ? this.itemTitleFor(pending.toTarget) : '';
|
|
2408
|
-
this.moveAnnouncement = pending.intent === 'attach'
|
|
2409
|
-
? `${pending.resourceTitle} is now attached to ${toTitle}.`
|
|
2410
|
-
: pending.intent === 'detach'
|
|
2411
|
-
? `${pending.resourceTitle} is now detached from ${fromTitle}.`
|
|
2412
|
-
: `${pending.resourceTitle} is now reassigned from ${fromTitle} to ${toTitle}.`;
|
|
2413
|
-
this.pendingResourceAssociationRequest = undefined;
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
private resourceAssociationAnnouncement(
|
|
2417
|
-
detailArg: THarnessResourceAssociationRequestDetail,
|
|
2418
|
-
completedArg: boolean,
|
|
2419
|
-
): string {
|
|
2420
|
-
const resourceTitle = detailArg.resource.title || detailArg.resource.id;
|
|
2421
|
-
const fromTitle = detailArg.fromTarget ? this.itemTitleFor(detailArg.fromTarget) : '';
|
|
2422
|
-
const toTitle = detailArg.toTarget ? this.itemTitleFor(detailArg.toTarget) : '';
|
|
2423
|
-
const verb = completedArg ? 'completed' : 'requested';
|
|
2424
|
-
return detailArg.intent === 'attach'
|
|
2425
|
-
? `Attach ${verb} for ${resourceTitle} to ${toTitle}.`
|
|
2426
|
-
: detailArg.intent === 'detach'
|
|
2427
|
-
? `Detach ${verb} for ${resourceTitle} from ${fromTitle}.`
|
|
2428
|
-
: `Reassign ${verb} for ${resourceTitle} from ${fromTitle} to ${toTitle}.`;
|
|
2429
|
-
}
|
|
2430
|
-
|
|
2431
2649
|
private async animateRowReflow(
|
|
2432
2650
|
previousRectsArg: Map<string, DOMRect>,
|
|
2433
2651
|
generationArg: number,
|
|
@@ -2470,7 +2688,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2470
2688
|
private finishPointerDrag(commitArg: boolean): void {
|
|
2471
2689
|
const gesture = this.pointerDragGesture;
|
|
2472
2690
|
if (!gesture) return;
|
|
2473
|
-
const
|
|
2691
|
+
const committing = commitArg && gesture.phase === 'active';
|
|
2692
|
+
// A centred release attaches instead of reordering; the two are never both pending.
|
|
2693
|
+
const attachTarget = committing && gesture.item.kind === 'resource'
|
|
2694
|
+
? this.dropAttachTarget
|
|
2695
|
+
: undefined;
|
|
2696
|
+
const moveDetail = (committing && !attachTarget && this.dropSlot)
|
|
2474
2697
|
? this.createMoveDetail(gesture.item, this.dropSlot.groupId, this.dropSlot.index, 'pointer')
|
|
2475
2698
|
: undefined;
|
|
2476
2699
|
this.pointerDragGesture = undefined;
|
|
@@ -2491,6 +2714,7 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2491
2714
|
this.draggingItem = undefined;
|
|
2492
2715
|
this.draggedItemHeight = 0;
|
|
2493
2716
|
this.dropSlot = undefined;
|
|
2717
|
+
this.dropAttachTarget = undefined;
|
|
2494
2718
|
try {
|
|
2495
2719
|
if (gesture.handle.hasPointerCapture(gesture.pointerId)) {
|
|
2496
2720
|
gesture.handle.releasePointerCapture(gesture.pointerId);
|
|
@@ -2498,6 +2722,19 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
2498
2722
|
} catch {
|
|
2499
2723
|
// Capture may already be released or the handle may have disconnected.
|
|
2500
2724
|
}
|
|
2725
|
+
if (attachTarget) {
|
|
2726
|
+
// Only an unattached or singly attached resource can reach this point, so the mode follows
|
|
2727
|
+
// from the current set without asking anything further.
|
|
2728
|
+
const current = this.resourceTargets(gesture.item.id);
|
|
2729
|
+
this.requestResourceAssociation(
|
|
2730
|
+
gesture.item.id,
|
|
2731
|
+
attachTarget,
|
|
2732
|
+
'pointer',
|
|
2733
|
+
current.length === 0 ? 'attach' : 'move',
|
|
2734
|
+
current[0],
|
|
2735
|
+
);
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
2501
2738
|
if (moveDetail) this.emitItemMove(moveDetail);
|
|
2502
2739
|
}
|
|
2503
2740
|
}
|