@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
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { demoFunc } from './dees-harness-conversation-picker.demo.js';
|
|
2
|
+
import {
|
|
3
|
+
DeesElement,
|
|
4
|
+
html,
|
|
5
|
+
css,
|
|
6
|
+
customElement,
|
|
7
|
+
type TemplateResult,
|
|
8
|
+
property,
|
|
9
|
+
state,
|
|
10
|
+
cssManager,
|
|
11
|
+
} from '@design.estate/dees-element';
|
|
12
|
+
import { themeDefaultStyles } from '../../00theme.js';
|
|
13
|
+
import type {
|
|
14
|
+
IHarnessResourceMeta,
|
|
15
|
+
IHarnessSessionGroup,
|
|
16
|
+
IHarnessSessionListItemRef,
|
|
17
|
+
IHarnessSessionMeta,
|
|
18
|
+
} from '../interfaces.js';
|
|
19
|
+
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
20
|
+
|
|
21
|
+
declare global {
|
|
22
|
+
interface HTMLElementTagNameMap {
|
|
23
|
+
'dees-harness-conversation-picker': DeesHarnessConversationPicker;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface IPickerEntry {
|
|
28
|
+
target: IHarnessSessionListItemRef;
|
|
29
|
+
title: string;
|
|
30
|
+
/** Project label, resource kind, or whatever second line the item offers. */
|
|
31
|
+
detail: string;
|
|
32
|
+
groupName: string;
|
|
33
|
+
icon: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const itemKey = (refArg: IHarnessSessionListItemRef): string => `${refArg.kind}:${refArg.id}`;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Stable per-option DOM id so the input can point `aria-activedescendant` at the active row.
|
|
40
|
+
* Ids only have to be unique inside this shadow root, and the key already is.
|
|
41
|
+
*/
|
|
42
|
+
const optionId = (refArg: IHarnessSessionListItemRef): string =>
|
|
43
|
+
`picker-option-${refArg.kind}-${refArg.id}`.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
44
|
+
|
|
45
|
+
const sameRef = (
|
|
46
|
+
leftArg: IHarnessSessionListItemRef | undefined,
|
|
47
|
+
rightArg: IHarnessSessionListItemRef | undefined,
|
|
48
|
+
): boolean => Boolean(leftArg && rightArg && itemKey(leftArg) === itemKey(rightArg));
|
|
49
|
+
|
|
50
|
+
/** Options for the one-call static helper. */
|
|
51
|
+
export interface IHarnessConversationPickerOptions {
|
|
52
|
+
sessions?: IHarnessSessionMeta[];
|
|
53
|
+
groups?: IHarnessSessionGroup[];
|
|
54
|
+
resources?: IHarnessResourceMeta[];
|
|
55
|
+
/** Targets already in use; filtered out so a picker never offers a no-op. */
|
|
56
|
+
excludeTargets?: IHarnessSessionListItemRef[];
|
|
57
|
+
/** Host-declared allowed targets; an absent list means unrestricted. */
|
|
58
|
+
eligibleTargets?: IHarnessSessionListItemRef[];
|
|
59
|
+
heading?: string;
|
|
60
|
+
subheading?: string;
|
|
61
|
+
emptyText?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Keyboard-first picker for one conversation (or one attachable resource). It is presentation
|
|
66
|
+
* only: it never mutates a session, a resource, or an association — it resolves one choice and
|
|
67
|
+
* leaves the change to the host.
|
|
68
|
+
*
|
|
69
|
+
* Typing filters on title, project label and group name. Up/Down move the active row, Enter picks
|
|
70
|
+
* it, Escape cancels. `harness-picker-choose` carries the chosen `IHarnessSessionListItemRef`;
|
|
71
|
+
* `harness-picker-cancel` carries nothing. Use the static `pick()` helper to get both as one
|
|
72
|
+
* promise inside a modal.
|
|
73
|
+
*/
|
|
74
|
+
@customElement('dees-harness-conversation-picker')
|
|
75
|
+
export class DeesHarnessConversationPicker extends DeesElement {
|
|
76
|
+
public static demo = demoFunc;
|
|
77
|
+
public static demoGroups = ['Agent Chat'];
|
|
78
|
+
|
|
79
|
+
@property({ attribute: false })
|
|
80
|
+
accessor sessions: IHarnessSessionMeta[] = [];
|
|
81
|
+
|
|
82
|
+
@property({ attribute: false })
|
|
83
|
+
accessor groups: IHarnessSessionGroup[] = [];
|
|
84
|
+
|
|
85
|
+
@property({ attribute: false })
|
|
86
|
+
accessor resources: IHarnessResourceMeta[] = [];
|
|
87
|
+
|
|
88
|
+
@property({ attribute: false })
|
|
89
|
+
accessor excludeTargets: IHarnessSessionListItemRef[] = [];
|
|
90
|
+
|
|
91
|
+
/** Absent (undefined) means unrestricted; an empty array means nothing is eligible. */
|
|
92
|
+
@property({ attribute: false })
|
|
93
|
+
accessor eligibleTargets: IHarnessSessionListItemRef[] | undefined = undefined;
|
|
94
|
+
|
|
95
|
+
@property({ type: String })
|
|
96
|
+
accessor heading: string = 'Attach to…';
|
|
97
|
+
|
|
98
|
+
@property({ type: String })
|
|
99
|
+
accessor emptyText: string = 'No conversation matches.';
|
|
100
|
+
|
|
101
|
+
@state()
|
|
102
|
+
accessor search: string = '';
|
|
103
|
+
|
|
104
|
+
@state()
|
|
105
|
+
accessor activeKey: string = '';
|
|
106
|
+
|
|
107
|
+
public static styles = [
|
|
108
|
+
themeDefaultStyles,
|
|
109
|
+
cssManager.defaultStyles,
|
|
110
|
+
css`
|
|
111
|
+
:host {
|
|
112
|
+
display: block;
|
|
113
|
+
min-width: 0;
|
|
114
|
+
font-family: var(--dees-font-family);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.searchRow {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
gap: var(--dees-spacing-sm);
|
|
121
|
+
padding: 0 0 var(--dees-spacing-sm);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.searchRow dees-icon {
|
|
125
|
+
flex: 0 0 auto;
|
|
126
|
+
color: var(--dees-color-text-secondary);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
input {
|
|
130
|
+
flex: 1;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
height: var(--dees-control-height-md);
|
|
133
|
+
padding: 0 10px;
|
|
134
|
+
border: 1px solid var(--dees-color-border-default);
|
|
135
|
+
border-radius: var(--dees-radius-md);
|
|
136
|
+
corner-shape: var(--dees-corner-shape);
|
|
137
|
+
background: var(--dees-color-control-bg);
|
|
138
|
+
color: var(--dees-color-text-primary);
|
|
139
|
+
font-family: inherit;
|
|
140
|
+
font-size: var(--dees-font-control-size-sm, 12px);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
input:focus-visible {
|
|
144
|
+
outline: 2px solid var(--dees-color-focus-ring);
|
|
145
|
+
outline-offset: 1px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.results {
|
|
149
|
+
display: grid;
|
|
150
|
+
gap: 2px;
|
|
151
|
+
max-height: 320px;
|
|
152
|
+
overflow-y: auto;
|
|
153
|
+
scrollbar-width: thin;
|
|
154
|
+
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.entry {
|
|
158
|
+
display: grid;
|
|
159
|
+
grid-template-columns: 20px minmax(0, 1fr);
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: var(--dees-spacing-sm);
|
|
162
|
+
padding: 6px 8px;
|
|
163
|
+
border: 0;
|
|
164
|
+
border-radius: var(--dees-radius-md);
|
|
165
|
+
corner-shape: var(--dees-corner-shape);
|
|
166
|
+
background: transparent;
|
|
167
|
+
color: var(--dees-color-text-primary);
|
|
168
|
+
cursor: pointer;
|
|
169
|
+
font-family: inherit;
|
|
170
|
+
text-align: left;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.entry:hover {
|
|
174
|
+
background: var(--dees-color-hover);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.entry.active {
|
|
178
|
+
background: var(--dees-color-nav-selected);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.entryCopy {
|
|
182
|
+
min-width: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.entryTitle {
|
|
186
|
+
display: block;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
text-overflow: ellipsis;
|
|
189
|
+
white-space: nowrap;
|
|
190
|
+
font-size: var(--dees-font-control-size-sm, 12px);
|
|
191
|
+
font-weight: 590;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.entryMeta {
|
|
195
|
+
display: block;
|
|
196
|
+
overflow: hidden;
|
|
197
|
+
margin-top: 1px;
|
|
198
|
+
color: var(--dees-color-text-secondary);
|
|
199
|
+
text-overflow: ellipsis;
|
|
200
|
+
white-space: nowrap;
|
|
201
|
+
font-size: var(--dees-font-caption2-size, 10px);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.empty {
|
|
205
|
+
padding: var(--dees-spacing-md) 8px;
|
|
206
|
+
color: var(--dees-color-text-secondary);
|
|
207
|
+
text-align: center;
|
|
208
|
+
font-size: var(--dees-font-control-size-sm, 12px);
|
|
209
|
+
}
|
|
210
|
+
`,
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/** Every offerable target, before the search filter. */
|
|
214
|
+
private get entries(): IPickerEntry[] {
|
|
215
|
+
const groupNameByKey = new Map<string, string>();
|
|
216
|
+
for (const group of this.groups) {
|
|
217
|
+
for (const ref of group.itemIds ?? []) {
|
|
218
|
+
if (ref) groupNameByKey.set(itemKey(ref), group.name);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const excluded = new Set(this.excludeTargets.filter(Boolean).map((ref) => itemKey(ref)));
|
|
222
|
+
const eligible = this.eligibleTargets
|
|
223
|
+
? new Set(this.eligibleTargets.filter(Boolean).map((ref) => itemKey(ref)))
|
|
224
|
+
: undefined;
|
|
225
|
+
const offerable = (refArg: IHarnessSessionListItemRef): boolean =>
|
|
226
|
+
!excluded.has(itemKey(refArg)) && (eligible === undefined || eligible.has(itemKey(refArg)));
|
|
227
|
+
|
|
228
|
+
const result: IPickerEntry[] = [];
|
|
229
|
+
for (const session of this.sessions) {
|
|
230
|
+
const target: IHarnessSessionListItemRef = { kind: 'session', id: session.id };
|
|
231
|
+
if (!session.id || !offerable(target)) continue;
|
|
232
|
+
result.push({
|
|
233
|
+
target,
|
|
234
|
+
title: session.title || session.id,
|
|
235
|
+
detail: session.projectLabel?.trim() || session.harness?.name || '',
|
|
236
|
+
groupName: groupNameByKey.get(itemKey(target)) ?? '',
|
|
237
|
+
icon: session.icon || 'lucide:MessageSquare',
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
for (const resource of this.resources) {
|
|
241
|
+
const target: IHarnessSessionListItemRef = { kind: 'resource', id: resource.id };
|
|
242
|
+
if (!resource.id || !offerable(target)) continue;
|
|
243
|
+
result.push({
|
|
244
|
+
target,
|
|
245
|
+
title: resource.title || resource.id,
|
|
246
|
+
detail: [resource.kind, resource.projectLabel?.trim()].filter(Boolean).join(' · '),
|
|
247
|
+
groupName: groupNameByKey.get(itemKey(target)) ?? '',
|
|
248
|
+
icon: resource.icon || 'lucide:Box',
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Title, project label and group name are all searchable, like the list itself. */
|
|
255
|
+
private get filteredEntries(): IPickerEntry[] {
|
|
256
|
+
const query = this.search.trim().toLowerCase();
|
|
257
|
+
if (!query) return this.entries;
|
|
258
|
+
return this.entries.filter((entry) => [entry.title, entry.detail, entry.groupName]
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.join(' ')
|
|
261
|
+
.toLowerCase()
|
|
262
|
+
.includes(query));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private get activeEntry(): IPickerEntry | undefined {
|
|
266
|
+
const entries = this.filteredEntries;
|
|
267
|
+
return entries.find((entry) => itemKey(entry.target) === this.activeKey) ?? entries[0];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
public render(): TemplateResult {
|
|
271
|
+
const entries = this.filteredEntries;
|
|
272
|
+
const active = this.activeEntry;
|
|
273
|
+
return html`
|
|
274
|
+
<div class="searchRow">
|
|
275
|
+
<dees-icon icon="lucide:Search" iconSize="14"></dees-icon>
|
|
276
|
+
<input
|
|
277
|
+
type="text"
|
|
278
|
+
role="combobox"
|
|
279
|
+
aria-expanded="true"
|
|
280
|
+
aria-controls="picker-results"
|
|
281
|
+
aria-activedescendant=${active ? optionId(active.target) : ''}
|
|
282
|
+
aria-label=${this.heading}
|
|
283
|
+
placeholder="Search conversations"
|
|
284
|
+
.value=${this.search}
|
|
285
|
+
@input=${(event: Event) => {
|
|
286
|
+
this.search = (event.target as HTMLInputElement).value;
|
|
287
|
+
this.activeKey = '';
|
|
288
|
+
}}
|
|
289
|
+
@keydown=${this.handleKeyDown}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
<div class="results" id="picker-results" role="listbox">
|
|
293
|
+
${entries.length
|
|
294
|
+
? entries.map((entry) => html`
|
|
295
|
+
<button
|
|
296
|
+
class=${`entry ${sameRef(entry.target, active?.target) ? 'active' : ''}`}
|
|
297
|
+
type="button"
|
|
298
|
+
role="option"
|
|
299
|
+
id=${optionId(entry.target)}
|
|
300
|
+
aria-selected=${sameRef(entry.target, active?.target) ? 'true' : 'false'}
|
|
301
|
+
data-target-key=${itemKey(entry.target)}
|
|
302
|
+
@click=${() => this.choose(entry.target)}
|
|
303
|
+
>
|
|
304
|
+
<dees-icon aria-hidden="true" icon=${entry.icon} iconSize="14"></dees-icon>
|
|
305
|
+
<span class="entryCopy">
|
|
306
|
+
<span class="entryTitle">${entry.title}</span>
|
|
307
|
+
${entry.detail || entry.groupName
|
|
308
|
+
? html`<span class="entryMeta">${[entry.detail, entry.groupName].filter(Boolean).join(' · ')}</span>`
|
|
309
|
+
: ''}
|
|
310
|
+
</span>
|
|
311
|
+
</button>
|
|
312
|
+
`)
|
|
313
|
+
: html`<div class="empty">${this.emptyText}</div>`}
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
public async focusSearch(): Promise<void> {
|
|
319
|
+
await this.updateComplete;
|
|
320
|
+
this.shadowRoot?.querySelector<HTMLInputElement>('input')?.focus();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private readonly handleKeyDown = (eventArg: KeyboardEvent): void => {
|
|
324
|
+
const entries = this.filteredEntries;
|
|
325
|
+
if (eventArg.key === 'Escape') {
|
|
326
|
+
eventArg.preventDefault();
|
|
327
|
+
eventArg.stopPropagation();
|
|
328
|
+
this.cancel();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (eventArg.key === 'Enter') {
|
|
332
|
+
eventArg.preventDefault();
|
|
333
|
+
const active = this.activeEntry;
|
|
334
|
+
if (active) this.choose(active.target);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (eventArg.key !== 'ArrowDown' && eventArg.key !== 'ArrowUp') return;
|
|
338
|
+
if (!entries.length) return;
|
|
339
|
+
eventArg.preventDefault();
|
|
340
|
+
const activeIndex = Math.max(
|
|
341
|
+
0,
|
|
342
|
+
entries.findIndex((entry) => itemKey(entry.target) === itemKey(this.activeEntry!.target)),
|
|
343
|
+
);
|
|
344
|
+
const nextIndex = eventArg.key === 'ArrowDown'
|
|
345
|
+
? (activeIndex + 1) % entries.length
|
|
346
|
+
: (activeIndex - 1 + entries.length) % entries.length;
|
|
347
|
+
this.activeKey = itemKey(entries[nextIndex]!.target);
|
|
348
|
+
void this.scrollActiveIntoView();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
private async scrollActiveIntoView(): Promise<void> {
|
|
352
|
+
await this.updateComplete;
|
|
353
|
+
this.shadowRoot?.querySelector<HTMLElement>('.entry.active')?.scrollIntoView({ block: 'nearest' });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private choose(targetArg: IHarnessSessionListItemRef): void {
|
|
357
|
+
this.dispatchEvent(new CustomEvent('harness-picker-choose', {
|
|
358
|
+
detail: { target: { kind: targetArg.kind, id: targetArg.id } },
|
|
359
|
+
bubbles: true,
|
|
360
|
+
composed: true,
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private cancel(): void {
|
|
365
|
+
this.dispatchEvent(new CustomEvent('harness-picker-cancel', { bubbles: true, composed: true }));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Opens the picker in a modal and resolves with the chosen target, or null when it was
|
|
370
|
+
* cancelled. Focus returns to whatever had it when the picker opened.
|
|
371
|
+
*/
|
|
372
|
+
public static async pick(
|
|
373
|
+
optionsArg: IHarnessConversationPickerOptions = {},
|
|
374
|
+
): Promise<IHarnessSessionListItemRef | null> {
|
|
375
|
+
const { DeesModal } = await import('../../00group-overlay/dees-modal/dees-modal.js');
|
|
376
|
+
const picker = new DeesHarnessConversationPicker();
|
|
377
|
+
picker.sessions = optionsArg.sessions ?? [];
|
|
378
|
+
picker.groups = optionsArg.groups ?? [];
|
|
379
|
+
picker.resources = optionsArg.resources ?? [];
|
|
380
|
+
picker.excludeTargets = optionsArg.excludeTargets ?? [];
|
|
381
|
+
picker.eligibleTargets = optionsArg.eligibleTargets;
|
|
382
|
+
picker.heading = optionsArg.heading ?? 'Attach to…';
|
|
383
|
+
if (optionsArg.emptyText) picker.emptyText = optionsArg.emptyText;
|
|
384
|
+
|
|
385
|
+
const previousFocus = (document.activeElement instanceof HTMLElement)
|
|
386
|
+
? document.activeElement
|
|
387
|
+
: undefined;
|
|
388
|
+
|
|
389
|
+
return new Promise<IHarnessSessionListItemRef | null>((resolve) => {
|
|
390
|
+
let settled = false;
|
|
391
|
+
const modalPromise = DeesModal.createAndShow({
|
|
392
|
+
heading: optionsArg.heading ?? 'Attach to…',
|
|
393
|
+
subheading: optionsArg.subheading,
|
|
394
|
+
content: html`${picker}`,
|
|
395
|
+
menuOptions: [],
|
|
396
|
+
width: 'small',
|
|
397
|
+
});
|
|
398
|
+
const finish = (resultArg: IHarnessSessionListItemRef | null): void => {
|
|
399
|
+
if (settled) return;
|
|
400
|
+
settled = true;
|
|
401
|
+
picker.removeEventListener('harness-picker-choose', onChoose);
|
|
402
|
+
picker.removeEventListener('harness-picker-cancel', onCancel);
|
|
403
|
+
void modalPromise.then((modal) => modal.destroy()).finally(() => {
|
|
404
|
+
previousFocus?.focus();
|
|
405
|
+
resolve(resultArg);
|
|
406
|
+
});
|
|
407
|
+
};
|
|
408
|
+
const onChoose = (eventArg: Event): void => {
|
|
409
|
+
finish((eventArg as CustomEvent<{ target: IHarnessSessionListItemRef }>).detail.target);
|
|
410
|
+
};
|
|
411
|
+
const onCancel = (): void => finish(null);
|
|
412
|
+
picker.addEventListener('harness-picker-choose', onChoose);
|
|
413
|
+
picker.addEventListener('harness-picker-cancel', onCancel);
|
|
414
|
+
void modalPromise.then(async (modal) => {
|
|
415
|
+
modal.addEventListener('modal-close', onCancel, { once: true });
|
|
416
|
+
await picker.focusSearch();
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dees-harness-conversation-picker.js';
|
package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { DeesElement, css, customElement, directives, html, state } from '@desig
|
|
|
2
2
|
import { demoApp, demoOutput, demoStyles, showDemoResult } from '../../../demos/templates.js';
|
|
3
3
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
4
4
|
import { harnessPillStyles } from '../harness.styles.js';
|
|
5
|
+
import { DeesHarnessConversationPicker } from '../dees-harness-conversation-picker/dees-harness-conversation-picker.js';
|
|
5
6
|
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
|
6
7
|
import '../dees-harness-question-card/dees-harness-question-card.js';
|
|
7
8
|
import '../dees-harness-permission-card/dees-harness-permission-card.js';
|
|
@@ -19,7 +20,7 @@ import type {
|
|
|
19
20
|
IHarnessSessionMeta,
|
|
20
21
|
IHarnessQuestionResponseDetail,
|
|
21
22
|
IHarnessPermissionResponseDetail,
|
|
22
|
-
|
|
23
|
+
THarnessResourceAssociateDetail,
|
|
23
24
|
} from '../interfaces.js';
|
|
24
25
|
|
|
25
26
|
type TDemoStage = 'idle' | 'starting' | 'working' | 'feedback' | 'approval' | 'finished' | 'error' | 'paused';
|
|
@@ -120,10 +121,14 @@ const demoResources: IHarnessResourceMeta[] = [
|
|
|
120
121
|
},
|
|
121
122
|
];
|
|
122
123
|
|
|
123
|
-
//
|
|
124
|
+
// A resource can be attached to several items at once, and a target is any other item: a session,
|
|
125
|
+
// or a resource such as a terminal.
|
|
124
126
|
const demoResourceAssociations: IHarnessResourceAssociation[] = [
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
+
{
|
|
128
|
+
resourceId: 'terminal:build',
|
|
129
|
+
targets: [{ kind: 'session', id: 'session-4' }, { kind: 'session', id: 'session-0' }],
|
|
130
|
+
},
|
|
131
|
+
{ resourceId: 'file:release-notes', targets: [{ kind: 'resource', id: 'terminal:build' }] },
|
|
127
132
|
];
|
|
128
133
|
|
|
129
134
|
const demoEligibleSessionTargets: IHarnessSessionListItemRef[] = [
|
|
@@ -235,19 +240,28 @@ const handleSessionContext = async (
|
|
|
235
240
|
});
|
|
236
241
|
};
|
|
237
242
|
|
|
243
|
+
const sameTarget = (
|
|
244
|
+
leftArg: IHarnessSessionListItemRef,
|
|
245
|
+
rightArg: IHarnessSessionListItemRef,
|
|
246
|
+
): boolean => leftArg.kind === rightArg.kind && leftArg.id === rightArg.id;
|
|
247
|
+
|
|
248
|
+
/** Applies one validated request to the controlled set; the list never mutates it itself. */
|
|
238
249
|
const handleResourceAssociation = (
|
|
239
|
-
event: CustomEvent<
|
|
250
|
+
event: CustomEvent<THarnessResourceAssociateDetail>,
|
|
240
251
|
): void => {
|
|
241
252
|
const list = event.currentTarget as TDemoSessionList;
|
|
253
|
+
const detail = event.detail;
|
|
254
|
+
const current = list.resourceAssociations
|
|
255
|
+
.find((association) => association.resourceId === detail.resource.id)?.targets ?? [];
|
|
256
|
+
const targets = detail.mode === 'detach'
|
|
257
|
+
? current.filter((target) => !sameTarget(target, detail.fromTarget))
|
|
258
|
+
: detail.mode === 'move'
|
|
259
|
+
? current.map((target) => (sameTarget(target, detail.fromTarget) ? { ...detail.toTarget } : target))
|
|
260
|
+
: [...current, { ...detail.toTarget }];
|
|
242
261
|
const next = list.resourceAssociations.filter(
|
|
243
|
-
(association) => association.resourceId !==
|
|
262
|
+
(association) => association.resourceId !== detail.resource.id,
|
|
244
263
|
);
|
|
245
|
-
if (
|
|
246
|
-
next.push({
|
|
247
|
-
resourceId: event.detail.resource.id,
|
|
248
|
-
target: { ...event.detail.toTarget },
|
|
249
|
-
});
|
|
250
|
-
}
|
|
264
|
+
if (targets.length) next.push({ resourceId: detail.resource.id, targets });
|
|
251
265
|
list.resourceAssociations = next;
|
|
252
266
|
};
|
|
253
267
|
|
|
@@ -255,36 +269,80 @@ const handleResourceContext = async (
|
|
|
255
269
|
event: CustomEvent<IHarnessResourceContextDetail>,
|
|
256
270
|
): Promise<void> => {
|
|
257
271
|
const list = event.currentTarget as TDemoSessionList;
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
: list.
|
|
272
|
+
const resource = event.detail.resource;
|
|
273
|
+
const currentTargets = event.detail.targets;
|
|
274
|
+
const eligibleTargets = list.eligibleTargetsByResourceId[resource.id];
|
|
275
|
+
const pickerOptions = {
|
|
276
|
+
sessions: list.sessions,
|
|
277
|
+
groups: list.groups,
|
|
278
|
+
resources: list.resources.filter((candidate) => candidate.id !== resource.id),
|
|
279
|
+
eligibleTargets,
|
|
280
|
+
};
|
|
281
|
+
/** Which attachment did the user mean? Only asked when there is more than one. */
|
|
282
|
+
const pickAttachment = async (
|
|
283
|
+
headingArg: string,
|
|
284
|
+
): Promise<IHarnessSessionListItemRef | null> => {
|
|
285
|
+
if (currentTargets.length <= 1) return currentTargets[0] ?? null;
|
|
286
|
+
return DeesHarnessConversationPicker.pick({
|
|
287
|
+
sessions: list.sessions,
|
|
288
|
+
groups: list.groups,
|
|
289
|
+
resources: list.resources,
|
|
290
|
+
eligibleTargets: currentTargets,
|
|
291
|
+
heading: headingArg,
|
|
292
|
+
emptyText: 'No attachment to choose.',
|
|
293
|
+
});
|
|
294
|
+
};
|
|
263
295
|
const menu = await DeesContextmenu.openContextMenuWithOptions(
|
|
264
296
|
new MouseEvent('contextmenu', {
|
|
265
297
|
clientX: event.detail.clientX,
|
|
266
298
|
clientY: event.detail.clientY,
|
|
267
299
|
}),
|
|
268
300
|
[
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
301
|
+
{
|
|
302
|
+
name: currentTargets.length ? 'Additionally attach to…' : 'Attach to…',
|
|
303
|
+
iconName: 'lucide:Link',
|
|
304
|
+
action: async () => {
|
|
305
|
+
event.detail.close();
|
|
306
|
+
const target = await DeesHarnessConversationPicker.pick({
|
|
307
|
+
...pickerOptions,
|
|
308
|
+
excludeTargets: currentTargets,
|
|
309
|
+
heading: `Attach ${resource.title} to…`,
|
|
310
|
+
});
|
|
311
|
+
if (target) {
|
|
312
|
+
event.detail.requestAssociation(
|
|
313
|
+
target,
|
|
314
|
+
currentTargets.length ? 'attach-additional' : 'attach',
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
...(currentTargets.length
|
|
320
|
+
? [
|
|
321
|
+
{
|
|
322
|
+
name: 'Move to…',
|
|
323
|
+
iconName: 'lucide:MoveRight',
|
|
324
|
+
action: async () => {
|
|
325
|
+
event.detail.close();
|
|
326
|
+
const fromTarget = await pickAttachment(`Move which attachment of ${resource.title}?`);
|
|
327
|
+
if (!fromTarget) return;
|
|
328
|
+
const target = await DeesHarnessConversationPicker.pick({
|
|
329
|
+
...pickerOptions,
|
|
330
|
+
excludeTargets: currentTargets,
|
|
331
|
+
heading: `Move ${resource.title} to…`,
|
|
332
|
+
});
|
|
333
|
+
if (target) event.detail.requestAssociation(target, 'move', fromTarget);
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
name: 'Detach',
|
|
338
|
+
iconName: 'lucide:Unlink',
|
|
339
|
+
action: async () => {
|
|
340
|
+
event.detail.close();
|
|
341
|
+
const fromTarget = await pickAttachment(`Detach which attachment of ${resource.title}?`);
|
|
342
|
+
if (fromTarget) event.detail.requestAssociation(null, 'detach', fromTarget);
|
|
343
|
+
},
|
|
286
344
|
},
|
|
287
|
-
|
|
345
|
+
]
|
|
288
346
|
: []),
|
|
289
347
|
],
|
|
290
348
|
);
|
|
@@ -462,7 +520,7 @@ class DeesDemoSessionWorkspace extends DeesElement {
|
|
|
462
520
|
this.notice = `Selected ${event.detail.resource.title}. Use Actions to attach or move it.`;
|
|
463
521
|
}}
|
|
464
522
|
@harness-resource-context=${handleResourceContext}
|
|
465
|
-
@harness-resource-
|
|
523
|
+
@harness-resource-associate=${(event: CustomEvent<THarnessResourceAssociateDetail>) => { handleResourceAssociation(event); this.syncList(); }}
|
|
466
524
|
></dees-harness-session-list>
|
|
467
525
|
<output role="status">${this.notice}</output>
|
|
468
526
|
</aside>
|