@nyaruka/temba-components 0.33.3 → 0.33.5
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/.github/workflows/build.yml +4 -5
- package/CHANGELOG.md +33 -3
- package/demo/index.html +1 -1
- package/dist/{7b0d8aca.js → 72be961a.js} +304 -78
- package/dist/index.js +304 -78
- package/dist/sw.js +1 -1
- package/dist/sw.js.map +1 -1
- package/dist/templates/components-body.html +1 -1
- package/dist/templates/components-head.html +1 -1
- package/out-tsc/src/FormElement.js +9 -6
- package/out-tsc/src/FormElement.js.map +1 -1
- package/out-tsc/src/contacts/ContactDetails.js +1 -1
- package/out-tsc/src/contacts/ContactDetails.js.map +1 -1
- package/out-tsc/src/contacts/ContactFields.js +6 -5
- package/out-tsc/src/contacts/ContactFields.js.map +1 -1
- package/out-tsc/src/contacts/ContactHistory.js +4 -4
- package/out-tsc/src/contacts/ContactHistory.js.map +1 -1
- package/out-tsc/src/fields/FieldManager.js +323 -0
- package/out-tsc/src/fields/FieldManager.js.map +1 -0
- package/out-tsc/src/interfaces.js +3 -0
- package/out-tsc/src/interfaces.js.map +1 -1
- package/out-tsc/src/list/SortableList.js +193 -0
- package/out-tsc/src/list/SortableList.js.map +1 -0
- package/out-tsc/src/store/Store.js +28 -17
- package/out-tsc/src/store/Store.js.map +1 -1
- package/out-tsc/src/store/StoreElement.js +1 -0
- package/out-tsc/src/store/StoreElement.js.map +1 -1
- package/out-tsc/src/vectoricon/index.js +7 -3
- package/out-tsc/src/vectoricon/index.js.map +1 -1
- package/out-tsc/temba-modules.js +4 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-field-manager.test.js +47 -0
- package/out-tsc/test/temba-field-manager.test.js.map +1 -0
- package/out-tsc/test/temba-sortable-list.test.js +48 -0
- package/out-tsc/test/temba-sortable-list.test.js.map +1 -0
- package/out-tsc/test/temba-store.test.js +1 -1
- package/out-tsc/test/temba-store.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/list/fields-dragging.png +0 -0
- package/screenshots/truth/list/fields-filtered.png +0 -0
- package/screenshots/truth/list/fields-hovered.png +0 -0
- package/screenshots/truth/list/fields.png +0 -0
- package/screenshots/truth/list/sortable-dragging.png +0 -0
- package/screenshots/truth/list/sortable-dropped.png +0 -0
- package/screenshots/truth/list/sortable.png +0 -0
- package/src/FormElement.ts +9 -6
- package/src/contacts/ContactDetails.ts +1 -1
- package/src/contacts/ContactFields.ts +6 -5
- package/src/contacts/ContactHistory.ts +4 -4
- package/src/fields/FieldManager.ts +353 -0
- package/src/interfaces.ts +5 -1
- package/src/list/SortableList.ts +224 -0
- package/src/store/Store.ts +34 -21
- package/src/store/StoreElement.ts +1 -0
- package/src/vectoricon/index.ts +7 -3
- package/static/svg/index.pdf +274 -0
- package/temba-modules.ts +4 -0
- package/test/temba-field-manager.test.ts +60 -0
- package/test/temba-sortable-list.test.ts +58 -0
- package/test/temba-store.test.ts +1 -1
- package/test-assets/store/fields.json +50 -5
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { css, html, PropertyValueMap, TemplateResult } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators';
|
|
3
|
+
import { ContactField, CustomEventType } from '../interfaces';
|
|
4
|
+
|
|
5
|
+
import { SortableList } from '../list/SortableList';
|
|
6
|
+
import { StoreElement } from '../store/StoreElement';
|
|
7
|
+
import { postJSON } from '../utils';
|
|
8
|
+
|
|
9
|
+
const TYPE_NAMES = {
|
|
10
|
+
text: 'Text',
|
|
11
|
+
numeric: 'Number',
|
|
12
|
+
number: 'Number',
|
|
13
|
+
datetime: 'Date & Time',
|
|
14
|
+
state: 'State',
|
|
15
|
+
ward: 'Ward',
|
|
16
|
+
district: 'District',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const matches = (field: ContactField, query: string): boolean => {
|
|
20
|
+
if (!query) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const search = (
|
|
24
|
+
field.label +
|
|
25
|
+
field.key +
|
|
26
|
+
TYPE_NAMES[field.value_type]
|
|
27
|
+
).toLowerCase();
|
|
28
|
+
if (search.toLowerCase().indexOf(query) > -1) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class FieldManager extends StoreElement {
|
|
35
|
+
static get styles() {
|
|
36
|
+
return css`
|
|
37
|
+
:host {
|
|
38
|
+
display: flex;
|
|
39
|
+
flex-grow: 1;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
min-height: 0px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.featured,
|
|
45
|
+
.other-fields {
|
|
46
|
+
background: #fff;
|
|
47
|
+
border-radius: var(--curvature);
|
|
48
|
+
box-shadow: var(--shadow);
|
|
49
|
+
margin-bottom: 1em;
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.featured {
|
|
55
|
+
max-height: 40%;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.other-fields {
|
|
59
|
+
flex-grow: 2;
|
|
60
|
+
min-height: 0px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
temba-textinput {
|
|
64
|
+
margin-bottom: 1em;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.scroll-box {
|
|
68
|
+
overflow-y: auto;
|
|
69
|
+
flex-grow: 1;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
display: flex;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.header temba-icon {
|
|
75
|
+
margin-right: 0.5em;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.label {
|
|
79
|
+
flex-grow: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.header {
|
|
83
|
+
padding: 0.5em 1em;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: flex-start;
|
|
86
|
+
border-bottom: 1px solid var(--color-widget-border);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.featured-field {
|
|
90
|
+
user-select: none;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
temba-sortable-list {
|
|
94
|
+
padding: 0.5em 0em;
|
|
95
|
+
width: 100%;
|
|
96
|
+
overflow-y: auto;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.scroll-box {
|
|
100
|
+
padding: 0.5em 0em;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
temba-icon[name='usages']:hover {
|
|
104
|
+
--icon-color: var(--color-link-primary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.field:hover temba-icon[name='delete_small'] {
|
|
108
|
+
opacity: 1 !important;
|
|
109
|
+
cursor: pointer !important;
|
|
110
|
+
pointer-events: all !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
temba-icon[name='delete_small']:hover {
|
|
114
|
+
--icon-color: var(--color-link-primary);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.field {
|
|
118
|
+
border: 1px solid transparent;
|
|
119
|
+
margin: 0 0.5em;
|
|
120
|
+
border-radius: var(--curvature);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.featured temba-sortable-list .field:hover {
|
|
124
|
+
cursor: move;
|
|
125
|
+
border-color: #e6e6e6;
|
|
126
|
+
background: #fcfcfc;
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@property({ type: String, attribute: 'priority-endpoint' })
|
|
132
|
+
priorityEndpoint: string;
|
|
133
|
+
|
|
134
|
+
@property({ type: Object, attribute: false })
|
|
135
|
+
featuredFields: ContactField[];
|
|
136
|
+
|
|
137
|
+
@property({ type: Object, attribute: false })
|
|
138
|
+
otherFieldKeys: string[] = [];
|
|
139
|
+
|
|
140
|
+
@property({ type: String })
|
|
141
|
+
draggingId: string;
|
|
142
|
+
|
|
143
|
+
@property({ type: String })
|
|
144
|
+
query = '';
|
|
145
|
+
|
|
146
|
+
protected firstUpdated(
|
|
147
|
+
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
148
|
+
): void {
|
|
149
|
+
super.firstUpdated(_changedProperties);
|
|
150
|
+
this.url = this.store.fieldsEndpoint;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private filterFields() {
|
|
154
|
+
const filteredKeys = this.store.getFieldKeys().filter(key => {
|
|
155
|
+
const field = this.store.getContactField(key);
|
|
156
|
+
if (field.featured) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return matches(field, this.query);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// sort by the label instead of the key
|
|
163
|
+
filteredKeys.sort((a, b) => {
|
|
164
|
+
return this.store
|
|
165
|
+
.getContactField(a)
|
|
166
|
+
.label.localeCompare(this.store.getContactField(b).label);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const featured: ContactField[] = [];
|
|
170
|
+
this.store.getFeaturedFields().forEach(field => {
|
|
171
|
+
if (matches(field, this.query)) {
|
|
172
|
+
featured.push(field);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.otherFieldKeys = filteredKeys;
|
|
177
|
+
this.featuredFields = featured;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
protected updated(
|
|
181
|
+
properties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
182
|
+
): void {
|
|
183
|
+
super.update(properties);
|
|
184
|
+
if (properties.has('data')) {
|
|
185
|
+
this.filterFields();
|
|
186
|
+
} else if (properties.has('query')) {
|
|
187
|
+
this.filterFields();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private handleSaveOrder(event) {
|
|
192
|
+
const list = event.currentTarget as SortableList;
|
|
193
|
+
postJSON(
|
|
194
|
+
this.priorityEndpoint,
|
|
195
|
+
list
|
|
196
|
+
.getIds()
|
|
197
|
+
.reverse()
|
|
198
|
+
.reduce((map, key, idx) => {
|
|
199
|
+
map[key] = idx;
|
|
200
|
+
return map;
|
|
201
|
+
}, {})
|
|
202
|
+
).then(() => {
|
|
203
|
+
this.store.refreshFields();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private handleOrderChanged(event) {
|
|
208
|
+
const swapsies = event.detail;
|
|
209
|
+
const temp = this.featuredFields[swapsies.fromIdx];
|
|
210
|
+
this.featuredFields[swapsies.fromIdx] = this.featuredFields[swapsies.toIdx];
|
|
211
|
+
this.featuredFields[swapsies.toIdx] = temp;
|
|
212
|
+
this.requestUpdate('featuredFields');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private handleDragStart(event) {
|
|
216
|
+
this.draggingId = event.detail.id;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private handleDragStop() {
|
|
220
|
+
this.draggingId = null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private handleFieldAction(event: MouseEvent) {
|
|
224
|
+
const ele = event.target as HTMLDivElement;
|
|
225
|
+
const key = ele.dataset.key;
|
|
226
|
+
const action = ele.dataset.action;
|
|
227
|
+
this.fireCustomEvent(CustomEventType.Selection, { key, action });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private handleSearch(event) {
|
|
231
|
+
this.query = (event.target.value || '').trim();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private hasUsages(field: ContactField): boolean {
|
|
235
|
+
return (
|
|
236
|
+
field.usages.campaign_events + field.usages.flows + field.usages.groups >
|
|
237
|
+
0
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private renderField(field: ContactField) {
|
|
242
|
+
return html`
|
|
243
|
+
<div
|
|
244
|
+
class="field sortable"
|
|
245
|
+
id="${field.key}"
|
|
246
|
+
style="
|
|
247
|
+
display: flex;
|
|
248
|
+
flex-direction: row;
|
|
249
|
+
align-items: center;
|
|
250
|
+
padding: 0.25em 1em;
|
|
251
|
+
${field.key === this.draggingId
|
|
252
|
+
? 'background: var(--color-selection)'
|
|
253
|
+
: ''}"
|
|
254
|
+
>
|
|
255
|
+
<div
|
|
256
|
+
style="display: flex; min-width: 200px; width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-right: 2em"
|
|
257
|
+
>
|
|
258
|
+
<span
|
|
259
|
+
@click=${this.handleFieldAction}
|
|
260
|
+
data-key=${field.key}
|
|
261
|
+
data-action="update"
|
|
262
|
+
style="color: var(--color-link-primary); cursor:pointer;"
|
|
263
|
+
>
|
|
264
|
+
${field.label}
|
|
265
|
+
</span>
|
|
266
|
+
${this.hasUsages(field)
|
|
267
|
+
? html`
|
|
268
|
+
<temba-icon
|
|
269
|
+
size="0.8"
|
|
270
|
+
style="color: #ccc; margin-left: 0.7em;"
|
|
271
|
+
name="usages"
|
|
272
|
+
data-key=${field.key}
|
|
273
|
+
data-action="usages"
|
|
274
|
+
@click=${this.handleFieldAction}
|
|
275
|
+
clickable
|
|
276
|
+
></temba-icon>
|
|
277
|
+
`
|
|
278
|
+
: null}
|
|
279
|
+
<div class="flex-grow:1"></div>
|
|
280
|
+
</div>
|
|
281
|
+
<div style="flex-grow:1; font-family: monospace; font-size:0.8em;">
|
|
282
|
+
@fields.${field.key}
|
|
283
|
+
</div>
|
|
284
|
+
<div>${TYPE_NAMES[field.value_type]}</div>
|
|
285
|
+
<temba-icon
|
|
286
|
+
style="pointer-events:none;color:#ccc;margin-left:0.3em;margin-right:-0.5em;opacity:0"
|
|
287
|
+
name="delete_small"
|
|
288
|
+
data-key=${field.key}
|
|
289
|
+
data-action="delete"
|
|
290
|
+
@click=${this.handleFieldAction}
|
|
291
|
+
></temba-icon>
|
|
292
|
+
</div>
|
|
293
|
+
`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
public render(): TemplateResult {
|
|
297
|
+
if (!this.featuredFields) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return html`
|
|
302
|
+
<temba-textinput
|
|
303
|
+
id="search"
|
|
304
|
+
placeholder="Search"
|
|
305
|
+
@change=${this.handleSearch}
|
|
306
|
+
clearable
|
|
307
|
+
></temba-textinput>
|
|
308
|
+
|
|
309
|
+
${this.featuredFields.length > 0
|
|
310
|
+
? html`
|
|
311
|
+
<div class="featured">
|
|
312
|
+
<div class="header">
|
|
313
|
+
<temba-icon name="featured"></temba-icon>
|
|
314
|
+
<div class="label">Featured</div>
|
|
315
|
+
</div>
|
|
316
|
+
${this.query
|
|
317
|
+
? html`
|
|
318
|
+
<div class="scroll-box">
|
|
319
|
+
${this.featuredFields.map(field =>
|
|
320
|
+
this.renderField(field)
|
|
321
|
+
)}
|
|
322
|
+
</div>
|
|
323
|
+
`
|
|
324
|
+
: html`
|
|
325
|
+
<temba-sortable-list
|
|
326
|
+
@change=${this.handleSaveOrder}
|
|
327
|
+
@temba-order-changed=${this.handleOrderChanged}
|
|
328
|
+
@temba-drag-start=${this.handleDragStart}
|
|
329
|
+
@temba-drag-stop=${this.handleDragStop}
|
|
330
|
+
>
|
|
331
|
+
${this.featuredFields.map(field =>
|
|
332
|
+
this.renderField(field)
|
|
333
|
+
)}
|
|
334
|
+
</temba-sortable-list>
|
|
335
|
+
`}
|
|
336
|
+
</div>
|
|
337
|
+
`
|
|
338
|
+
: null}
|
|
339
|
+
|
|
340
|
+
<div class="other-fields">
|
|
341
|
+
<div class="header">
|
|
342
|
+
<temba-icon name="fields"></temba-icon>
|
|
343
|
+
<div class="label">Everything Else</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div class="scroll-box">
|
|
346
|
+
${this.otherFieldKeys.map(field =>
|
|
347
|
+
this.renderField(this.store.getContactField(field))
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
</div>
|
|
351
|
+
`;
|
|
352
|
+
}
|
|
353
|
+
}
|
package/src/interfaces.ts
CHANGED
|
@@ -97,8 +97,9 @@ export interface ContactField {
|
|
|
97
97
|
key: string;
|
|
98
98
|
label: string;
|
|
99
99
|
value_type: string;
|
|
100
|
-
|
|
100
|
+
featured: boolean;
|
|
101
101
|
priority: number;
|
|
102
|
+
usages: { campaign_events: number; flows: number; groups: number };
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
export interface ContactGroup {
|
|
@@ -239,4 +240,7 @@ export enum CustomEventType {
|
|
|
239
240
|
NoPath = 'temba-no-path',
|
|
240
241
|
StoreUpdated = 'temba-store-updated',
|
|
241
242
|
Ready = 'temba-ready',
|
|
243
|
+
OrderChanged = 'temba-order-changed',
|
|
244
|
+
DragStart = 'temba-drag-start',
|
|
245
|
+
DragStop = 'temba-drag-stop',
|
|
242
246
|
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { css, html, PropertyValueMap, TemplateResult } from 'lit';
|
|
2
|
+
import { property } from 'lit/decorators';
|
|
3
|
+
import { CustomEventType } from '../interfaces';
|
|
4
|
+
import { RapidElement } from '../RapidElement';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A simple list that can be sorted by dragging
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// how far we have to drag before it starts
|
|
11
|
+
const DRAG_THRESHOLD = 5;
|
|
12
|
+
export class SortableList extends RapidElement {
|
|
13
|
+
static get styles() {
|
|
14
|
+
return css`
|
|
15
|
+
:host {
|
|
16
|
+
margin: auto;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.container {
|
|
20
|
+
user-select: none;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dragging {
|
|
24
|
+
background: var(--color-selection);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.sortable {
|
|
28
|
+
transition: all 300ms ease-in-out;
|
|
29
|
+
display: flex;
|
|
30
|
+
padding: 0.4em 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.sortable:hover temba-icon {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
cursor: move;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.ghost {
|
|
39
|
+
position: absolute;
|
|
40
|
+
opacity: 0.5;
|
|
41
|
+
transition: none;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.slot {
|
|
45
|
+
flex-grow: 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
slot > * {
|
|
49
|
+
user-select: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
temba-icon {
|
|
53
|
+
opacity: 0.1;
|
|
54
|
+
padding: 0.2em 0.5em;
|
|
55
|
+
transition: all 300ms ease-in-out;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@property({ type: String })
|
|
61
|
+
draggingId: string;
|
|
62
|
+
|
|
63
|
+
ghostElement: HTMLDivElement = null;
|
|
64
|
+
downEle: HTMLDivElement = null;
|
|
65
|
+
xOffset = 0;
|
|
66
|
+
yOffset = 0;
|
|
67
|
+
yDown = 0;
|
|
68
|
+
|
|
69
|
+
draggingIdx = -1;
|
|
70
|
+
draggingEle = null;
|
|
71
|
+
|
|
72
|
+
public constructor() {
|
|
73
|
+
super();
|
|
74
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
75
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
76
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected firstUpdated(
|
|
80
|
+
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
81
|
+
): void {
|
|
82
|
+
super.firstUpdated(_changedProperties);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public getIds() {
|
|
86
|
+
return this.shadowRoot
|
|
87
|
+
.querySelector('slot')
|
|
88
|
+
.assignedElements()
|
|
89
|
+
.map(ele => ele.id);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private getRowIndex(id: string): number {
|
|
93
|
+
return this.shadowRoot
|
|
94
|
+
.querySelector('slot')
|
|
95
|
+
.assignedElements()
|
|
96
|
+
.findIndex(ele => ele.id === id);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private getOverlappingElement(mouseY: number): HTMLDivElement {
|
|
100
|
+
const ghostRect = this.ghostElement.getBoundingClientRect();
|
|
101
|
+
|
|
102
|
+
const ele = this.shadowRoot
|
|
103
|
+
.querySelector('slot')
|
|
104
|
+
.assignedElements()
|
|
105
|
+
.find(otherEle => {
|
|
106
|
+
const rect = otherEle.getBoundingClientRect();
|
|
107
|
+
|
|
108
|
+
// don't return ourselves
|
|
109
|
+
if (otherEle.id === this.ghostElement.id) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (mouseY > this.yDown) {
|
|
114
|
+
// moving down
|
|
115
|
+
return ghostRect.top < rect.bottom && ghostRect.bottom > rect.bottom;
|
|
116
|
+
} else {
|
|
117
|
+
// moving up
|
|
118
|
+
return rect.top < ghostRect.bottom && rect.bottom > ghostRect.bottom;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return ele as HTMLDivElement;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private handleMouseDown(event: MouseEvent) {
|
|
125
|
+
let ele = event.target as HTMLDivElement;
|
|
126
|
+
ele = ele.closest('.sortable');
|
|
127
|
+
if (ele) {
|
|
128
|
+
this.downEle = ele;
|
|
129
|
+
this.draggingId = ele.id;
|
|
130
|
+
this.draggingIdx = this.getRowIndex(ele.id);
|
|
131
|
+
this.draggingEle = ele;
|
|
132
|
+
|
|
133
|
+
this.xOffset = event.clientX - ele.offsetLeft;
|
|
134
|
+
this.yOffset = event.clientY - ele.offsetTop;
|
|
135
|
+
this.yDown = event.clientY;
|
|
136
|
+
|
|
137
|
+
document.addEventListener('mousemove', this.handleMouseMove);
|
|
138
|
+
document.addEventListener('mouseup', this.handleMouseUp);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private handleMouseMove(event: MouseEvent) {
|
|
143
|
+
const scrollTop = this.shadowRoot
|
|
144
|
+
.querySelector('slot')
|
|
145
|
+
.assignedElements()[0].parentElement.scrollTop;
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
!this.ghostElement &&
|
|
149
|
+
this.downEle &&
|
|
150
|
+
Math.abs(event.clientY - this.yDown) > DRAG_THRESHOLD
|
|
151
|
+
) {
|
|
152
|
+
this.fireCustomEvent(CustomEventType.DragStart, {
|
|
153
|
+
id: this.downEle.id,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
this.ghostElement = this.downEle.cloneNode(true) as HTMLDivElement;
|
|
157
|
+
this.ghostElement.classList.add('ghost');
|
|
158
|
+
|
|
159
|
+
const computedStyle = getComputedStyle(this.downEle);
|
|
160
|
+
|
|
161
|
+
this.ghostElement.style.width =
|
|
162
|
+
this.downEle.clientWidth -
|
|
163
|
+
parseFloat(computedStyle.paddingLeft) -
|
|
164
|
+
parseFloat(computedStyle.paddingRight) +
|
|
165
|
+
'px';
|
|
166
|
+
const container = this.shadowRoot.querySelector('.container');
|
|
167
|
+
|
|
168
|
+
container.appendChild(this.ghostElement);
|
|
169
|
+
|
|
170
|
+
this.downEle = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (this.ghostElement) {
|
|
174
|
+
this.ghostElement.style.left = event.clientX - this.xOffset + 'px';
|
|
175
|
+
this.ghostElement.style.top =
|
|
176
|
+
event.clientY - this.yOffset - scrollTop + 'px';
|
|
177
|
+
|
|
178
|
+
const other = this.getOverlappingElement(event.clientY);
|
|
179
|
+
if (other) {
|
|
180
|
+
const otherIdx = this.getRowIndex(other.id);
|
|
181
|
+
const dragId = this.ghostElement.id;
|
|
182
|
+
const otherId = other.id;
|
|
183
|
+
|
|
184
|
+
this.fireCustomEvent(CustomEventType.OrderChanged, {
|
|
185
|
+
from: dragId,
|
|
186
|
+
to: otherId,
|
|
187
|
+
fromIdx: this.draggingIdx,
|
|
188
|
+
toIdx: otherIdx,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// TODO: Dont do swapping, just send the full order?
|
|
192
|
+
this.draggingIdx = otherIdx;
|
|
193
|
+
this.draggingId = otherId;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private handleMouseUp() {
|
|
199
|
+
if (this.draggingId) {
|
|
200
|
+
this.fireCustomEvent(CustomEventType.DragStop, {
|
|
201
|
+
id: this.draggingId,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.draggingId = null;
|
|
205
|
+
this.downEle = null;
|
|
206
|
+
|
|
207
|
+
if (this.ghostElement) {
|
|
208
|
+
this.ghostElement.remove();
|
|
209
|
+
this.ghostElement = null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
document.removeEventListener('mousemove', this.handleMouseMove);
|
|
213
|
+
document.removeEventListener('mouseup', this.handleMouseUp);
|
|
214
|
+
this.dispatchEvent(new Event('change'));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
public render(): TemplateResult {
|
|
218
|
+
return html`
|
|
219
|
+
<div class="container">
|
|
220
|
+
<slot @mousedown=${this.handleMouseDown}></slot>
|
|
221
|
+
</div>
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
}
|
package/src/store/Store.ts
CHANGED
|
@@ -64,7 +64,7 @@ export class Store extends RapidElement {
|
|
|
64
64
|
private groups: { [uuid: string]: ContactGroup } = {};
|
|
65
65
|
private languages: any = {};
|
|
66
66
|
private workspace: Workspace;
|
|
67
|
-
private
|
|
67
|
+
private featuredFields: ContactField[] = [];
|
|
68
68
|
|
|
69
69
|
private langService = new HumanizeDurationLanguage();
|
|
70
70
|
private humanizer = new HumanizeDuration(this.langService);
|
|
@@ -103,24 +103,7 @@ export class Store extends RapidElement {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
if (this.fieldsEndpoint) {
|
|
106
|
-
fetches.push(
|
|
107
|
-
getAssets(this.fieldsEndpoint).then((assets: Asset[]) => {
|
|
108
|
-
this.keyedAssets['fields'] = [];
|
|
109
|
-
this.pinnedFields = [];
|
|
110
|
-
|
|
111
|
-
assets.forEach((field: ContactField) => {
|
|
112
|
-
this.keyedAssets['fields'].push(field.key);
|
|
113
|
-
this.fields[field.key] = field;
|
|
114
|
-
if (field.pinned) {
|
|
115
|
-
this.pinnedFields.push(field);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
this.pinnedFields.sort((a, b) => {
|
|
120
|
-
return b.priority - a.priority;
|
|
121
|
-
});
|
|
122
|
-
})
|
|
123
|
-
);
|
|
106
|
+
fetches.push(this.refreshFields());
|
|
124
107
|
}
|
|
125
108
|
|
|
126
109
|
if (this.globalsEndpoint) {
|
|
@@ -183,6 +166,32 @@ export class Store extends RapidElement {
|
|
|
183
166
|
return 'en';
|
|
184
167
|
}
|
|
185
168
|
|
|
169
|
+
public refreshFields() {
|
|
170
|
+
return getAssets(this.fieldsEndpoint).then((assets: Asset[]) => {
|
|
171
|
+
this.keyedAssets['fields'] = [];
|
|
172
|
+
this.featuredFields = [];
|
|
173
|
+
|
|
174
|
+
assets.forEach((field: ContactField) => {
|
|
175
|
+
this.keyedAssets['fields'].push(field.key);
|
|
176
|
+
this.fields[field.key] = field;
|
|
177
|
+
if (field.featured) {
|
|
178
|
+
this.featuredFields.push(field);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
this.featuredFields.sort((a, b) => {
|
|
183
|
+
return b.priority - a.priority;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
this.keyedAssets['fields'].sort();
|
|
187
|
+
|
|
188
|
+
this.fireCustomEvent(CustomEventType.StoreUpdated, {
|
|
189
|
+
url: this.fieldsEndpoint,
|
|
190
|
+
data: this.keyedAssets['fields'],
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
public getShortDuration(
|
|
187
196
|
isoDateA: string,
|
|
188
197
|
isoDateB: string = null,
|
|
@@ -232,12 +241,16 @@ export class Store extends RapidElement {
|
|
|
232
241
|
return this.keyedAssets;
|
|
233
242
|
}
|
|
234
243
|
|
|
244
|
+
public getFieldKeys(): string[] {
|
|
245
|
+
return this.keyedAssets['fields'];
|
|
246
|
+
}
|
|
247
|
+
|
|
235
248
|
public getContactField(key: string): ContactField {
|
|
236
249
|
return this.fields[key];
|
|
237
250
|
}
|
|
238
251
|
|
|
239
|
-
public
|
|
240
|
-
return this.
|
|
252
|
+
public getFeaturedFields(): ContactField[] {
|
|
253
|
+
return this.featuredFields;
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
public getLanguageName(iso: string) {
|