@chisquare-tech/origine-ngx-apps-design 0.1.0 → 0.2.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.
|
@@ -5,6 +5,7 @@ import { map, forkJoin, concat, last } from 'rxjs';
|
|
|
5
5
|
import * as i1 from '@angular/forms';
|
|
6
6
|
import { FormsModule } from '@angular/forms';
|
|
7
7
|
import { RouterLink, Router, ActivatedRoute } from '@angular/router';
|
|
8
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
8
9
|
import { marked } from 'marked';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -123,6 +124,34 @@ class DesignApi {
|
|
|
123
124
|
deleteView(project, id) {
|
|
124
125
|
return this.api.delete(`${D}/project/${enc(project)}/view/${enc(id)}`);
|
|
125
126
|
}
|
|
127
|
+
react(commentId, reaction, on) {
|
|
128
|
+
const url = `${D}/comment/${enc(commentId)}/reaction/${reaction}`;
|
|
129
|
+
return (on ? this.api.put(url, {}) : this.api.delete(url)).pipe(map((r) => data(r) ?? []));
|
|
130
|
+
}
|
|
131
|
+
attachments(key) {
|
|
132
|
+
return this.api.get(`${D}/issue/${enc(key)}/attachment`).pipe(map((r) => data(r) ?? []));
|
|
133
|
+
}
|
|
134
|
+
/** Uploads a file as the raw request body (at most 25 MB). */
|
|
135
|
+
attach(key, file) {
|
|
136
|
+
return this.api
|
|
137
|
+
.post(`${D}/issue/${enc(key)}/attachment?filename=${encodeURIComponent(file.name)}`, null, {
|
|
138
|
+
body: file, headers: { 'Content-Type': file.type || 'application/octet-stream' },
|
|
139
|
+
})
|
|
140
|
+
.pipe(map((r) => data(r)));
|
|
141
|
+
}
|
|
142
|
+
/** A file's bytes (the content endpoint needs the session, so no plain link works). */
|
|
143
|
+
attachmentBlob(id) {
|
|
144
|
+
return this.api.get(`${D}/attachment/${enc(id)}/content`, null, { responseType: 'blob' }).pipe(map((b) => b));
|
|
145
|
+
}
|
|
146
|
+
deleteAttachment(id) {
|
|
147
|
+
return this.api.delete(`${D}/attachment/${enc(id)}`);
|
|
148
|
+
}
|
|
149
|
+
watchState(key) {
|
|
150
|
+
return this.api.get('v1/links/watch', { kind: 'issue', ref: key }).pipe(map((r) => data(r) ?? { watching: false, watchers: [] }));
|
|
151
|
+
}
|
|
152
|
+
watch(key, on) {
|
|
153
|
+
return on ? this.api.put('v1/links/watch', { kind: 'issue', ref: key }) : this.api.delete(`v1/links/watch?kind=issue&ref=${enc(key)}`);
|
|
154
|
+
}
|
|
126
155
|
development(key) {
|
|
127
156
|
return this.api.get(`v1/links/issue/${enc(key)}/development`).pipe(map((r) => data(r) ?? { branches: [], commits: [], pulls: [] }));
|
|
128
157
|
}
|
|
@@ -277,6 +306,10 @@ function historyLine(h, people) {
|
|
|
277
306
|
return `${who} removed the link to ${shown(h.old, people)}`;
|
|
278
307
|
case 'closed_by':
|
|
279
308
|
return `${who} closed it by merging ${shown(h.new, people)}`;
|
|
309
|
+
case 'attached':
|
|
310
|
+
return `${who} attached ${typeof h.new === 'string' ? h.new : 'a file'}`;
|
|
311
|
+
case 'attachment_deleted':
|
|
312
|
+
return `${who} deleted the attachment ${typeof h.old === 'string' ? h.old : ''}`.trim();
|
|
280
313
|
default:
|
|
281
314
|
return `${who} ${h.kind.replace(/_/g, ' ')}`;
|
|
282
315
|
}
|
|
@@ -1858,6 +1891,15 @@ const REL_LABEL = {
|
|
|
1858
1891
|
blocks: 'blocks', blocked_by: 'is blocked by', relates: 'relates to', duplicates: 'duplicates', duplicated_by: 'is duplicated by',
|
|
1859
1892
|
};
|
|
1860
1893
|
const SEVERITIES = ['critical', 'major', 'minor', 'trivial'];
|
|
1894
|
+
const REACTIONS = [
|
|
1895
|
+
{ name: '+1', emoji: '👍', label: 'thumbs up' }, { name: '-1', emoji: '👎', label: 'thumbs down' }, { name: 'laugh', emoji: '😄', label: 'laugh' },
|
|
1896
|
+
{ name: 'hooray', emoji: '🎉', label: 'hooray' }, { name: 'confused', emoji: '😕', label: 'confused' }, { name: 'heart', emoji: '❤️', label: 'heart' },
|
|
1897
|
+
{ name: 'rocket', emoji: '🚀', label: 'rocket' }, { name: 'eyes', emoji: '👀', label: 'eyes' },
|
|
1898
|
+
];
|
|
1899
|
+
const MAX_FILE = 25 * 1024 * 1024;
|
|
1900
|
+
const MENTION = /\[@([^\]\n]{1,120})\]\(mention:([A-Za-z0-9_-]{1,64})\)/g;
|
|
1901
|
+
const esc = (s) => s.replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
1902
|
+
const size = (n) => (n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(0)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`);
|
|
1861
1903
|
// /{org}/{project}/issues/PAY-142 (prototype "design_issue"): the issue's description (Markdown),
|
|
1862
1904
|
// sub-tasks, what code names it, linked issues, comments and history; its fields on the right, each
|
|
1863
1905
|
// editable in place by those who edit issues. Every change names the version it read, so a change
|
|
@@ -1888,6 +1930,18 @@ class IssuePage {
|
|
|
1888
1930
|
this.copied = signal(false, ...(ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []));
|
|
1889
1931
|
this.confirmDelete = signal(false, ...(ngDevMode ? [{ debugName: "confirmDelete" }] : /* istanbul ignore next */ []));
|
|
1890
1932
|
this.deleteKey = signal('', ...(ngDevMode ? [{ debugName: "deleteKey" }] : /* istanbul ignore next */ []));
|
|
1933
|
+
this.attachments = signal([], ...(ngDevMode ? [{ debugName: "attachments" }] : /* istanbul ignore next */ []));
|
|
1934
|
+
this.thumbs = signal({}, ...(ngDevMode ? [{ debugName: "thumbs" }] : /* istanbul ignore next */ []));
|
|
1935
|
+
this.uploading = signal(false, ...(ngDevMode ? [{ debugName: "uploading" }] : /* istanbul ignore next */ []));
|
|
1936
|
+
this.watching = signal(false, ...(ngDevMode ? [{ debugName: "watching" }] : /* istanbul ignore next */ []));
|
|
1937
|
+
this.watchers = signal([], ...(ngDevMode ? [{ debugName: "watchers" }] : /* istanbul ignore next */ []));
|
|
1938
|
+
this.picker = signal(null, ...(ngDevMode ? [{ debugName: "picker" }] : /* istanbul ignore next */ []));
|
|
1939
|
+
this.mentionFor = signal(null, ...(ngDevMode ? [{ debugName: "mentionFor" }] : /* istanbul ignore next */ []));
|
|
1940
|
+
this.mentionQuery = signal('', ...(ngDevMode ? [{ debugName: "mentionQuery" }] : /* istanbul ignore next */ []));
|
|
1941
|
+
this.mentionIndex = signal(0, ...(ngDevMode ? [{ debugName: "mentionIndex" }] : /* istanbul ignore next */ []));
|
|
1942
|
+
this.mentionStart = 0;
|
|
1943
|
+
this.reactionList = REACTIONS;
|
|
1944
|
+
this.size = size;
|
|
1891
1945
|
this.titleDraft = '';
|
|
1892
1946
|
this.descDraft = '';
|
|
1893
1947
|
this.linkKind = 'relates';
|
|
@@ -1917,6 +1971,22 @@ class IssuePage {
|
|
|
1917
1971
|
return id ? this.sprints().find((s) => s.id === id && s.state === 'completed') ?? null : null;
|
|
1918
1972
|
}, ...(ngDevMode ? [{ debugName: "completedSprint" }] : /* istanbul ignore next */ []));
|
|
1919
1973
|
this.html = computed(() => this.md(this.issue()?.description ?? ''), ...(ngDevMode ? [{ debugName: "html" }] : /* istanbul ignore next */ []));
|
|
1974
|
+
this.watcherNames = computed(() => {
|
|
1975
|
+
const me = this.host.meId();
|
|
1976
|
+
const ids = this.watchers();
|
|
1977
|
+
if (!ids.length)
|
|
1978
|
+
return 'nobody yet';
|
|
1979
|
+
const names = ids.map((id) => (id === me ? 'you' : this.people().name(id)));
|
|
1980
|
+
names.sort((a, b) => (a === 'you' ? -1 : b === 'you' ? 1 : 0));
|
|
1981
|
+
return names.join(', ');
|
|
1982
|
+
}, ...(ngDevMode ? [{ debugName: "watcherNames" }] : /* istanbul ignore next */ []));
|
|
1983
|
+
this.suggestions = computed(() => {
|
|
1984
|
+
const q = this.mentionQuery().toLowerCase();
|
|
1985
|
+
const me = this.host.meId();
|
|
1986
|
+
return this.host.people()
|
|
1987
|
+
.filter((p) => p.id !== me && (!q || (p.name ?? '').toLowerCase().split(/\s+/).some((w) => w.startsWith(q)) || (p.name ?? '').toLowerCase().startsWith(q) || (p.email ?? '').toLowerCase().startsWith(q)))
|
|
1988
|
+
.slice(0, 6);
|
|
1989
|
+
}, ...(ngDevMode ? [{ debugName: "suggestions" }] : /* istanbul ignore next */ []));
|
|
1920
1990
|
this.route.paramMap.subscribe((m) => this.key.set((m.get('key') ?? '').toUpperCase()));
|
|
1921
1991
|
effect(() => {
|
|
1922
1992
|
const key = this.key();
|
|
@@ -1925,8 +1995,148 @@ class IssuePage {
|
|
|
1925
1995
|
untracked(() => this.loadAll(key, p.id));
|
|
1926
1996
|
});
|
|
1927
1997
|
}
|
|
1998
|
+
/** Markdown to HTML; a mention shows the person's current name, not the label typed with it. */
|
|
1928
1999
|
md(src) {
|
|
1929
|
-
|
|
2000
|
+
const people = this.people();
|
|
2001
|
+
const withMentions = (src || '').replace(MENTION, (_m, label, id) => {
|
|
2002
|
+
const name = people.name(id);
|
|
2003
|
+
return `<span class="mention">@${esc(name && name !== id ? name : label)}</span>`;
|
|
2004
|
+
});
|
|
2005
|
+
return marked.parse(withMentions, { async: false, gfm: true, breaks: true });
|
|
2006
|
+
}
|
|
2007
|
+
// ---- mentions ----
|
|
2008
|
+
typed(el, which) {
|
|
2009
|
+
const before = el.value.slice(0, el.selectionStart ?? el.value.length);
|
|
2010
|
+
const m = /(^|\s)@([\p{L}\w.-]{0,30})$/u.exec(before);
|
|
2011
|
+
if (!m) {
|
|
2012
|
+
this.mentionFor.set(null);
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
this.mentionStart = before.length - m[2].length - 1;
|
|
2016
|
+
this.mentionQuery.set(m[2]);
|
|
2017
|
+
this.mentionIndex.set(0);
|
|
2018
|
+
this.mentionFor.set(which);
|
|
2019
|
+
}
|
|
2020
|
+
mentionKey(ev, el) {
|
|
2021
|
+
if (!this.mentionFor() || !this.suggestions().length)
|
|
2022
|
+
return;
|
|
2023
|
+
const n = this.suggestions().length;
|
|
2024
|
+
if (ev.key === 'ArrowDown') {
|
|
2025
|
+
ev.preventDefault();
|
|
2026
|
+
this.mentionIndex.set((this.mentionIndex() + 1) % n);
|
|
2027
|
+
}
|
|
2028
|
+
else if (ev.key === 'ArrowUp') {
|
|
2029
|
+
ev.preventDefault();
|
|
2030
|
+
this.mentionIndex.set((this.mentionIndex() + n - 1) % n);
|
|
2031
|
+
}
|
|
2032
|
+
else if (ev.key === 'Enter' || ev.key === 'Tab') {
|
|
2033
|
+
ev.preventDefault();
|
|
2034
|
+
this.pick(this.suggestions()[this.mentionIndex()], el);
|
|
2035
|
+
}
|
|
2036
|
+
else if (ev.key === 'Escape') {
|
|
2037
|
+
ev.preventDefault();
|
|
2038
|
+
this.mentionFor.set(null);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
closeMentionSoon() {
|
|
2042
|
+
setTimeout(() => this.mentionFor.set(null), 150);
|
|
2043
|
+
}
|
|
2044
|
+
pick(p, el) {
|
|
2045
|
+
const caret = el.selectionStart ?? el.value.length;
|
|
2046
|
+
const token = `[@${(p.name || p.email || 'someone').replace(/[\[\]]/g, '')}](mention:${p.id}) `;
|
|
2047
|
+
const next = el.value.slice(0, this.mentionStart) + token + el.value.slice(caret);
|
|
2048
|
+
if (this.mentionFor() === 'desc')
|
|
2049
|
+
this.descDraft = next;
|
|
2050
|
+
else
|
|
2051
|
+
this.draft.set(next);
|
|
2052
|
+
el.value = next;
|
|
2053
|
+
const at = this.mentionStart + token.length;
|
|
2054
|
+
this.mentionFor.set(null);
|
|
2055
|
+
setTimeout(() => { el.focus(); el.setSelectionRange(at, at); });
|
|
2056
|
+
}
|
|
2057
|
+
// ---- reactions ----
|
|
2058
|
+
emoji(r) { return REACTIONS.find((x) => x.name === r)?.emoji ?? r; }
|
|
2059
|
+
emojiLabel(r) { return REACTIONS.find((x) => x.name === r)?.label ?? r; }
|
|
2060
|
+
reactors(ids) {
|
|
2061
|
+
const me = this.host.meId();
|
|
2062
|
+
return ids.map((id) => (id === me ? 'You' : this.people().name(id))).join(', ');
|
|
2063
|
+
}
|
|
2064
|
+
react(c, r, on) {
|
|
2065
|
+
this.picker.set(null);
|
|
2066
|
+
this.api.react(c.id, r, on).subscribe({
|
|
2067
|
+
next: (rs) => this.comments.set(this.comments().map((x) => (x.id === c.id ? { ...x, reactions: rs } : x))),
|
|
2068
|
+
error: (e) => this.fail(e, 'Could not react.'),
|
|
2069
|
+
});
|
|
2070
|
+
}
|
|
2071
|
+
// ---- watching ----
|
|
2072
|
+
toggleWatch() {
|
|
2073
|
+
const is = this.issue();
|
|
2074
|
+
if (!is)
|
|
2075
|
+
return;
|
|
2076
|
+
const on = !this.watching();
|
|
2077
|
+
this.api.watch(is.key, on).subscribe({ next: () => this.loadWatch(is.key), error: (e) => this.fail(e, 'Could not change watching.') });
|
|
2078
|
+
}
|
|
2079
|
+
loadWatch(key) {
|
|
2080
|
+
this.api.watchState(key).subscribe({ next: (w) => { this.watching.set(w.watching); this.watchers.set(w.watchers); }, error: () => undefined });
|
|
2081
|
+
}
|
|
2082
|
+
// ---- attachments ----
|
|
2083
|
+
ext(name) {
|
|
2084
|
+
const i = name.lastIndexOf('.');
|
|
2085
|
+
return i > 0 && name.length - i <= 5 ? name.slice(i + 1) : 'file';
|
|
2086
|
+
}
|
|
2087
|
+
upload(input) {
|
|
2088
|
+
const is = this.issue();
|
|
2089
|
+
const files = Array.from(input.files ?? []);
|
|
2090
|
+
input.value = '';
|
|
2091
|
+
if (!is || !files.length)
|
|
2092
|
+
return;
|
|
2093
|
+
const big = files.find((f) => f.size > MAX_FILE);
|
|
2094
|
+
if (big) {
|
|
2095
|
+
this.error.set(`${big.name} is larger than 25 MB.`);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
this.uploading.set(true);
|
|
2099
|
+
this.error.set('');
|
|
2100
|
+
forkJoin(files.map((f) => this.api.attach(is.key, f))).subscribe({
|
|
2101
|
+
next: () => { this.uploading.set(false); this.loadAttachments(is.key); this.reloadSide(is.key); },
|
|
2102
|
+
error: (e) => { this.uploading.set(false); this.loadAttachments(is.key); this.fail(e, 'Could not attach the file.'); },
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
download(a) {
|
|
2106
|
+
this.api.attachmentBlob(a.id).subscribe({
|
|
2107
|
+
next: (b) => {
|
|
2108
|
+
const url = URL.createObjectURL(b);
|
|
2109
|
+
const link = document.createElement('a');
|
|
2110
|
+
link.href = url;
|
|
2111
|
+
link.download = a.filename;
|
|
2112
|
+
link.click();
|
|
2113
|
+
setTimeout(() => URL.revokeObjectURL(url), 10_000);
|
|
2114
|
+
},
|
|
2115
|
+
error: (e) => this.fail(e, 'Could not download the file.'),
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
removeAttachment(a) {
|
|
2119
|
+
const is = this.issue();
|
|
2120
|
+
if (!is)
|
|
2121
|
+
return;
|
|
2122
|
+
this.api.deleteAttachment(a.id).subscribe({
|
|
2123
|
+
next: () => { this.loadAttachments(is.key); this.reloadSide(is.key); },
|
|
2124
|
+
error: (e) => this.fail(e, 'Could not delete the file.'),
|
|
2125
|
+
});
|
|
2126
|
+
}
|
|
2127
|
+
loadAttachments(key) {
|
|
2128
|
+
this.api.attachments(key).subscribe({
|
|
2129
|
+
next: (list) => {
|
|
2130
|
+
this.attachments.set(list);
|
|
2131
|
+
const have = this.thumbs();
|
|
2132
|
+
for (const a of list) {
|
|
2133
|
+
if (a.inline && !have[a.id] && a.size <= 5 * 1024 * 1024) {
|
|
2134
|
+
this.api.attachmentBlob(a.id).subscribe({ next: (b) => this.thumbs.set({ ...this.thumbs(), [a.id]: URL.createObjectURL(b) }), error: () => undefined });
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
},
|
|
2138
|
+
error: () => undefined,
|
|
2139
|
+
});
|
|
1930
2140
|
}
|
|
1931
2141
|
line(h) {
|
|
1932
2142
|
return historyLine(h, this.people());
|
|
@@ -2042,7 +2252,7 @@ class IssuePage {
|
|
|
2042
2252
|
return;
|
|
2043
2253
|
this.busy.set(true);
|
|
2044
2254
|
this.api.comment(is.key, body).subscribe({
|
|
2045
|
-
next: (c) => { this.busy.set(false); this.draft.set(''); this.comments.set([...this.comments(), c]); this.reloadSide(is.key); },
|
|
2255
|
+
next: (c) => { this.busy.set(false); this.draft.set(''); this.comments.set([...this.comments(), { ...c, reactions: [] }]); this.reloadSide(is.key); setTimeout(() => this.loadWatch(is.key), 800); },
|
|
2046
2256
|
error: (e) => this.fail(e, 'Could not add the comment.'),
|
|
2047
2257
|
});
|
|
2048
2258
|
}
|
|
@@ -2089,6 +2299,11 @@ class IssuePage {
|
|
|
2089
2299
|
this.editingTitle.set(false);
|
|
2090
2300
|
this.editingDesc.set(false);
|
|
2091
2301
|
this.confirmDelete.set(false);
|
|
2302
|
+
this.attachments.set([]);
|
|
2303
|
+
this.watchers.set([]);
|
|
2304
|
+
this.watching.set(false);
|
|
2305
|
+
this.loadAttachments(key);
|
|
2306
|
+
this.loadWatch(key);
|
|
2092
2307
|
forkJoin({ is: this.api.get(key), c: this.api.comments(key), h: this.api.history(key) }).subscribe({
|
|
2093
2308
|
next: ({ is, c, h }) => { this.issue.set(is); this.comments.set(c); this.history.set([...h].reverse()); },
|
|
2094
2309
|
error: (e) => this.error.set(e?.status === 404 ? `There is no issue ${key} you can see in this project.` : apiMessage(e, `Could not open ${key}.`)),
|
|
@@ -2112,6 +2327,7 @@ class IssuePage {
|
|
|
2112
2327
|
<span class="o-mono" aria-current="page">{{ is.key }}</span>
|
|
2113
2328
|
</nav>
|
|
2114
2329
|
<span class="o-r o-flex">
|
|
2330
|
+
<button type="button" class="o-btn o-sm" [attr.aria-pressed]="watching()" (click)="toggleWatch()" data-testid="watch">{{ watching() ? 'Watching' : 'Watch' }}</button>
|
|
2115
2331
|
<button type="button" class="o-btn o-sm" (click)="copyLink()">{{ copied() ? 'Copied' : 'Copy link' }}</button>
|
|
2116
2332
|
<a class="o-btn o-sm" [routerLink]="host.link('board')">Board</a>
|
|
2117
2333
|
</span>
|
|
@@ -2136,9 +2352,11 @@ class IssuePage {
|
|
|
2136
2352
|
</div>
|
|
2137
2353
|
|
|
2138
2354
|
@if (editingDesc()) {
|
|
2139
|
-
<div class="o-card">
|
|
2355
|
+
<div class="o-card mn">
|
|
2140
2356
|
<label class="o-sr-only" for="is-desc">Description</label>
|
|
2141
|
-
<textarea id="is-desc" class="o-in" style="border:none;box-shadow:none" [(ngModel)]="descDraft" placeholder="Write with Markdown: checklists, code blocks, images and @mentions." data-testid="desc-input"
|
|
2357
|
+
<textarea #descEl id="is-desc" class="o-in" style="border:none;box-shadow:none" [(ngModel)]="descDraft" placeholder="Write with Markdown: checklists, code blocks, images and @mentions." data-testid="desc-input"
|
|
2358
|
+
(input)="typed(descEl, 'desc')" (keydown)="mentionKey($event, descEl)" (blur)="closeMentionSoon()" role="combobox" [attr.aria-expanded]="mentionFor() === 'desc'" aria-autocomplete="list"></textarea>
|
|
2359
|
+
@if (mentionFor() === 'desc' && suggestions().length) {<ng-container *ngTemplateOutlet="mentionList; context: { el: descEl }" />}
|
|
2142
2360
|
<div class="o-cf"><span>Markdown</span><button type="button" class="o-btn o-sm o-r" (click)="editingDesc.set(false)">Cancel</button><button type="button" class="o-btn o-sm o-dark" (click)="saveDesc()" [disabled]="busy()" data-testid="desc-save">Save</button></div>
|
|
2143
2361
|
</div>
|
|
2144
2362
|
} @else {
|
|
@@ -2229,6 +2447,24 @@ class IssuePage {
|
|
|
2229
2447
|
}
|
|
2230
2448
|
</div>
|
|
2231
2449
|
|
|
2450
|
+
@if (attachments().length) {
|
|
2451
|
+
<div class="o-card" data-testid="attachments">
|
|
2452
|
+
<div class="o-ch"><b>Attachments</b><span class="o-faint o-sm">{{ attachments().length }}</span>
|
|
2453
|
+
@if (canEdit()) {<label class="o-lnk o-r" for="att-in-2">+ Attach</label>}</div>
|
|
2454
|
+
@for (a of attachments(); track a.id) {
|
|
2455
|
+
<div class="o-row" [attr.data-testid]="'attachment-' + a.filename">
|
|
2456
|
+
@if (a.inline && thumbs()[a.id]) {<img class="att-img" [src]="thumbs()[a.id]" [alt]="a.filename" />} @else {<span class="att-ic" aria-hidden="true">{{ ext(a.filename) }}</span>}
|
|
2457
|
+
<div class="o-col" style="gap:2px;min-width:0;flex:1">
|
|
2458
|
+
<button type="button" class="o-lnk o-ell" style="text-align:left" (click)="download(a)">{{ a.filename }}</button>
|
|
2459
|
+
<span class="o-faint o-xs">{{ size(a.size) }} · {{ people().name(a.uploadedBy) }} · {{ ago(a.createdAt) }}</span>
|
|
2460
|
+
</div>
|
|
2461
|
+
@if ((a.uploadedBy === host.meId() && canEdit()) || canManage()) {<button type="button" class="o-btn o-ghost o-xs" (click)="removeAttachment(a)" [attr.aria-label]="'Delete ' + a.filename">×</button>}
|
|
2462
|
+
</div>
|
|
2463
|
+
}
|
|
2464
|
+
@if (canEdit()) {<input id="att-in-2" class="file-in" type="file" multiple (change)="upload($any($event.target))" />}
|
|
2465
|
+
</div>
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2232
2468
|
<div class="o-col" style="gap:12px">
|
|
2233
2469
|
<div class="o-tabs" role="tablist">
|
|
2234
2470
|
<button type="button" role="tab" class="o-tab" [class.o-on]="tab() === 'comments'" [attr.aria-selected]="tab() === 'comments'" (click)="tab.set('comments')">Comments<span class="o-n">{{ comments().length }}</span></button>
|
|
@@ -2242,14 +2478,36 @@ class IssuePage {
|
|
|
2242
2478
|
<span class="o-faint o-sm">{{ ago(c.createdAt) }}{{ c.edited ? ' · edited' : '' }}</span>
|
|
2243
2479
|
@if (c.authorId === host.meId() || canManage()) {<button type="button" class="o-lnk o-r" style="color:var(--o-mut)" (click)="removeComment(c)">Delete</button>}
|
|
2244
2480
|
</div>
|
|
2245
|
-
<div class="o-cb o-lh
|
|
2481
|
+
<div class="o-cb o-lh" style="font-size:14px">
|
|
2482
|
+
<div class="o-md md" [innerHTML]="md(c.body)"></div>
|
|
2483
|
+
<div class="rx">
|
|
2484
|
+
@for (r of c.reactions ?? []; track r.reaction) {
|
|
2485
|
+
<button type="button" class="o-chip o-out" [class.mine]="r.mine" [attr.aria-pressed]="r.mine" [disabled]="!canComment()" (click)="react(c, r.reaction, !r.mine)"
|
|
2486
|
+
[attr.aria-label]="emojiLabel(r.reaction) + ': ' + r.count + (r.mine ? ', including you' : '')" [title]="reactors(r.people)">{{ emoji(r.reaction) }} {{ r.count }}</button>
|
|
2487
|
+
}
|
|
2488
|
+
@if (canComment()) {
|
|
2489
|
+
@if (picker() === c.id) {
|
|
2490
|
+
<span class="rx-pal" role="group" aria-label="Add a reaction">
|
|
2491
|
+
@for (x of reactionList; track x.name) {<button type="button" [attr.aria-label]="x.label" (click)="react(c, x.name, true)">{{ x.emoji }}</button>}
|
|
2492
|
+
</span>
|
|
2493
|
+
} @else {
|
|
2494
|
+
<button type="button" class="o-chip o-out" style="color:var(--o-mut)" (click)="picker.set(c.id)" aria-label="Add a reaction" data-testid="react">☺︎ +</button>
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
</div>
|
|
2498
|
+
</div>
|
|
2246
2499
|
</div>
|
|
2247
2500
|
}
|
|
2248
2501
|
@if (canComment()) {
|
|
2249
|
-
<div class="o-card">
|
|
2502
|
+
<div class="o-card mn">
|
|
2250
2503
|
<label class="o-sr-only" for="is-cmt">Add a comment</label>
|
|
2251
|
-
<textarea id="is-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;font-family:var(--o-font);font-size:13px" placeholder="Add a comment. Markdown
|
|
2252
|
-
|
|
2504
|
+
<textarea #cmtEl id="is-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;font-family:var(--o-font);font-size:13px" placeholder="Add a comment. Markdown and @mentions work." [ngModel]="draft()" (ngModelChange)="draft.set($event)" data-testid="comment-input"
|
|
2505
|
+
(input)="typed(cmtEl, 'comment')" (keydown)="mentionKey($event, cmtEl)" (blur)="closeMentionSoon()" role="combobox" [attr.aria-expanded]="mentionFor() === 'comment'" aria-autocomplete="list"></textarea>
|
|
2506
|
+
@if (mentionFor() === 'comment' && suggestions().length) {<ng-container *ngTemplateOutlet="mentionList; context: { el: cmtEl }" />}
|
|
2507
|
+
<div class="o-cf">
|
|
2508
|
+
@if (canEdit()) {<label for="att-in" class="o-lnk" style="color:var(--o-mut)">{{ uploading() ? 'Uploading…' : 'Attach files up to 25 MB' }}</label><input id="att-in" class="file-in" type="file" multiple (change)="upload($any($event.target))" data-testid="attach-input" />} @else {<span>Markdown</span>}
|
|
2509
|
+
<button type="button" class="o-btn o-sm o-dark o-r" (click)="addComment()" [disabled]="!draft().trim() || busy()" data-testid="comment-add">Comment</button>
|
|
2510
|
+
</div>
|
|
2253
2511
|
</div>
|
|
2254
2512
|
}
|
|
2255
2513
|
} @else {
|
|
@@ -2329,6 +2587,7 @@ class IssuePage {
|
|
|
2329
2587
|
<span class="o-faint">Created {{ shortDate(is.createdAt) }} by {{ people().name(is.reporterId) }}</span>
|
|
2330
2588
|
<span class="o-faint">Updated {{ ago(is.updatedAt) }}</span>
|
|
2331
2589
|
@if (is.resolvedAt) {<span class="o-faint">Resolved {{ shortDate(is.resolvedAt) }} · {{ is.resolution }}</span>}
|
|
2590
|
+
<span class="o-faint" data-testid="watchers">Watching: {{ watcherNames() }}</span>
|
|
2332
2591
|
</div>
|
|
2333
2592
|
@if (canManage()) {
|
|
2334
2593
|
@if (confirmDelete()) {
|
|
@@ -2349,11 +2608,20 @@ class IssuePage {
|
|
|
2349
2608
|
} @else {
|
|
2350
2609
|
<div class="o-page"><div class="o-card"><div class="o-empty">Loading {{ key() }}…</div></div></div>
|
|
2351
2610
|
}
|
|
2352
|
-
|
|
2611
|
+
<ng-template #mentionList let-el="el">
|
|
2612
|
+
<ul class="mn-list" role="listbox" aria-label="People to mention">
|
|
2613
|
+
@for (p of suggestions(); track p.id; let i = $index) {
|
|
2614
|
+
<li role="option" [class.on]="i === mentionIndex()" [attr.aria-selected]="i === mentionIndex()" (mousedown)="$event.preventDefault(); pick(p, el)">
|
|
2615
|
+
<span class="o-av" aria-hidden="true">{{ people().initials(p.id) }}</span><span class="o-ell">{{ p.name || p.email }}</span>@if (p.email && p.name) {<span class="o-faint o-xs o-ell">{{ p.email }}</span>}
|
|
2616
|
+
</li>
|
|
2617
|
+
}
|
|
2618
|
+
</ul>
|
|
2619
|
+
</ng-template>
|
|
2620
|
+
`, isInline: true, styles: [".ti{font-size:24px;margin:0;line-height:1.25;text-wrap:pretty}.ti-btn{border:none;background:none;padding:0;text-align:left;font:inherit;color:inherit;cursor:text}.md :where(p,ul,ol,pre){margin:0}.md pre{background:var(--o-well);padding:10px 12px;border-radius:8px;overflow:auto;font-family:var(--o-mono);font-size:12px}.md code{font-family:var(--o-mono);font-size:12px;color:var(--o-tx);background:var(--o-sunken);padding:0 4px;border-radius:4px}.md pre code{background:none;padding:0}.md img{max-width:100%}.kv-in{width:100%}textarea.o-in{min-height:140px;font-family:var(--o-mono);font-size:12px;line-height:1.6;padding:10px 12px;height:auto}.md .mention{color:var(--o-acc-tx, var(--o-acc));background:var(--o-acc-bg, var(--o-sunken));border-radius:4px;padding:0 3px;font-weight:500}.rx{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;align-items:center}.rx .o-chip{cursor:pointer;font-size:12px}.rx .o-chip.mine{border-color:var(--o-acc);background:var(--o-acc-bg, var(--o-sunken))}.rx-pal{display:flex;gap:2px;padding:4px;border:1px solid var(--o-line);border-radius:8px;background:var(--o-surface);box-shadow:var(--o-shadow-sm, none)}.rx-pal button{border:none;background:none;font-size:16px;padding:2px 4px;border-radius:6px;cursor:pointer}.rx-pal button:hover,.rx-pal button:focus-visible{background:var(--o-sunken)}.mn{position:relative}.mn-list{position:absolute;left:8px;top:calc(100% - 4px);z-index:20;min-width:220px;max-width:320px;border:1px solid var(--o-line);border-radius:8px;background:var(--o-surface);box-shadow:var(--o-shadow, 0 6px 20px rgba(0,0,0,.12));padding:4px;list-style:none;margin:0}.mn-list li{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:13px}.mn-list li.on,.mn-list li:hover{background:var(--o-sunken)}.att-img{width:40px;height:40px;border-radius:6px;object-fit:cover;background:var(--o-well);flex:none;overflow:hidden;font-size:0}.att-ic{width:40px;height:40px;border-radius:6px;background:var(--o-well);display:grid;place-items:center;font-size:10px;color:var(--o-mut);font-family:var(--o-mono);flex:none;text-transform:uppercase}.file-in{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i1.NgForm, selector: "form:not([ngNoForm]):not([formGroup]):not([formArray]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: RouterLink, selector: "[routerLink]", inputs: ["target", "queryParams", "fragment", "queryParamsHandling", "state", "info", "relativeTo", "preserveFragment", "skipLocationChange", "replaceUrl", "routerLink"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2353
2621
|
}
|
|
2354
2622
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImport: i0, type: IssuePage, decorators: [{
|
|
2355
2623
|
type: Component,
|
|
2356
|
-
args: [{ selector: 'o-design-issue', imports: [FormsModule, RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2624
|
+
args: [{ selector: 'o-design-issue', imports: [FormsModule, RouterLink, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
2357
2625
|
@if (issue(); as is) {
|
|
2358
2626
|
<div class="o-page o-wide">
|
|
2359
2627
|
<div class="o-flex o-sm o-wrap">
|
|
@@ -2365,6 +2633,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2365
2633
|
<span class="o-mono" aria-current="page">{{ is.key }}</span>
|
|
2366
2634
|
</nav>
|
|
2367
2635
|
<span class="o-r o-flex">
|
|
2636
|
+
<button type="button" class="o-btn o-sm" [attr.aria-pressed]="watching()" (click)="toggleWatch()" data-testid="watch">{{ watching() ? 'Watching' : 'Watch' }}</button>
|
|
2368
2637
|
<button type="button" class="o-btn o-sm" (click)="copyLink()">{{ copied() ? 'Copied' : 'Copy link' }}</button>
|
|
2369
2638
|
<a class="o-btn o-sm" [routerLink]="host.link('board')">Board</a>
|
|
2370
2639
|
</span>
|
|
@@ -2389,9 +2658,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2389
2658
|
</div>
|
|
2390
2659
|
|
|
2391
2660
|
@if (editingDesc()) {
|
|
2392
|
-
<div class="o-card">
|
|
2661
|
+
<div class="o-card mn">
|
|
2393
2662
|
<label class="o-sr-only" for="is-desc">Description</label>
|
|
2394
|
-
<textarea id="is-desc" class="o-in" style="border:none;box-shadow:none" [(ngModel)]="descDraft" placeholder="Write with Markdown: checklists, code blocks, images and @mentions." data-testid="desc-input"
|
|
2663
|
+
<textarea #descEl id="is-desc" class="o-in" style="border:none;box-shadow:none" [(ngModel)]="descDraft" placeholder="Write with Markdown: checklists, code blocks, images and @mentions." data-testid="desc-input"
|
|
2664
|
+
(input)="typed(descEl, 'desc')" (keydown)="mentionKey($event, descEl)" (blur)="closeMentionSoon()" role="combobox" [attr.aria-expanded]="mentionFor() === 'desc'" aria-autocomplete="list"></textarea>
|
|
2665
|
+
@if (mentionFor() === 'desc' && suggestions().length) {<ng-container *ngTemplateOutlet="mentionList; context: { el: descEl }" />}
|
|
2395
2666
|
<div class="o-cf"><span>Markdown</span><button type="button" class="o-btn o-sm o-r" (click)="editingDesc.set(false)">Cancel</button><button type="button" class="o-btn o-sm o-dark" (click)="saveDesc()" [disabled]="busy()" data-testid="desc-save">Save</button></div>
|
|
2396
2667
|
</div>
|
|
2397
2668
|
} @else {
|
|
@@ -2482,6 +2753,24 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2482
2753
|
}
|
|
2483
2754
|
</div>
|
|
2484
2755
|
|
|
2756
|
+
@if (attachments().length) {
|
|
2757
|
+
<div class="o-card" data-testid="attachments">
|
|
2758
|
+
<div class="o-ch"><b>Attachments</b><span class="o-faint o-sm">{{ attachments().length }}</span>
|
|
2759
|
+
@if (canEdit()) {<label class="o-lnk o-r" for="att-in-2">+ Attach</label>}</div>
|
|
2760
|
+
@for (a of attachments(); track a.id) {
|
|
2761
|
+
<div class="o-row" [attr.data-testid]="'attachment-' + a.filename">
|
|
2762
|
+
@if (a.inline && thumbs()[a.id]) {<img class="att-img" [src]="thumbs()[a.id]" [alt]="a.filename" />} @else {<span class="att-ic" aria-hidden="true">{{ ext(a.filename) }}</span>}
|
|
2763
|
+
<div class="o-col" style="gap:2px;min-width:0;flex:1">
|
|
2764
|
+
<button type="button" class="o-lnk o-ell" style="text-align:left" (click)="download(a)">{{ a.filename }}</button>
|
|
2765
|
+
<span class="o-faint o-xs">{{ size(a.size) }} · {{ people().name(a.uploadedBy) }} · {{ ago(a.createdAt) }}</span>
|
|
2766
|
+
</div>
|
|
2767
|
+
@if ((a.uploadedBy === host.meId() && canEdit()) || canManage()) {<button type="button" class="o-btn o-ghost o-xs" (click)="removeAttachment(a)" [attr.aria-label]="'Delete ' + a.filename">×</button>}
|
|
2768
|
+
</div>
|
|
2769
|
+
}
|
|
2770
|
+
@if (canEdit()) {<input id="att-in-2" class="file-in" type="file" multiple (change)="upload($any($event.target))" />}
|
|
2771
|
+
</div>
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2485
2774
|
<div class="o-col" style="gap:12px">
|
|
2486
2775
|
<div class="o-tabs" role="tablist">
|
|
2487
2776
|
<button type="button" role="tab" class="o-tab" [class.o-on]="tab() === 'comments'" [attr.aria-selected]="tab() === 'comments'" (click)="tab.set('comments')">Comments<span class="o-n">{{ comments().length }}</span></button>
|
|
@@ -2495,14 +2784,36 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2495
2784
|
<span class="o-faint o-sm">{{ ago(c.createdAt) }}{{ c.edited ? ' · edited' : '' }}</span>
|
|
2496
2785
|
@if (c.authorId === host.meId() || canManage()) {<button type="button" class="o-lnk o-r" style="color:var(--o-mut)" (click)="removeComment(c)">Delete</button>}
|
|
2497
2786
|
</div>
|
|
2498
|
-
<div class="o-cb o-lh
|
|
2787
|
+
<div class="o-cb o-lh" style="font-size:14px">
|
|
2788
|
+
<div class="o-md md" [innerHTML]="md(c.body)"></div>
|
|
2789
|
+
<div class="rx">
|
|
2790
|
+
@for (r of c.reactions ?? []; track r.reaction) {
|
|
2791
|
+
<button type="button" class="o-chip o-out" [class.mine]="r.mine" [attr.aria-pressed]="r.mine" [disabled]="!canComment()" (click)="react(c, r.reaction, !r.mine)"
|
|
2792
|
+
[attr.aria-label]="emojiLabel(r.reaction) + ': ' + r.count + (r.mine ? ', including you' : '')" [title]="reactors(r.people)">{{ emoji(r.reaction) }} {{ r.count }}</button>
|
|
2793
|
+
}
|
|
2794
|
+
@if (canComment()) {
|
|
2795
|
+
@if (picker() === c.id) {
|
|
2796
|
+
<span class="rx-pal" role="group" aria-label="Add a reaction">
|
|
2797
|
+
@for (x of reactionList; track x.name) {<button type="button" [attr.aria-label]="x.label" (click)="react(c, x.name, true)">{{ x.emoji }}</button>}
|
|
2798
|
+
</span>
|
|
2799
|
+
} @else {
|
|
2800
|
+
<button type="button" class="o-chip o-out" style="color:var(--o-mut)" (click)="picker.set(c.id)" aria-label="Add a reaction" data-testid="react">☺︎ +</button>
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
</div>
|
|
2804
|
+
</div>
|
|
2499
2805
|
</div>
|
|
2500
2806
|
}
|
|
2501
2807
|
@if (canComment()) {
|
|
2502
|
-
<div class="o-card">
|
|
2808
|
+
<div class="o-card mn">
|
|
2503
2809
|
<label class="o-sr-only" for="is-cmt">Add a comment</label>
|
|
2504
|
-
<textarea id="is-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;font-family:var(--o-font);font-size:13px" placeholder="Add a comment. Markdown
|
|
2505
|
-
|
|
2810
|
+
<textarea #cmtEl id="is-cmt" class="o-in" style="border:none;box-shadow:none;min-height:70px;font-family:var(--o-font);font-size:13px" placeholder="Add a comment. Markdown and @mentions work." [ngModel]="draft()" (ngModelChange)="draft.set($event)" data-testid="comment-input"
|
|
2811
|
+
(input)="typed(cmtEl, 'comment')" (keydown)="mentionKey($event, cmtEl)" (blur)="closeMentionSoon()" role="combobox" [attr.aria-expanded]="mentionFor() === 'comment'" aria-autocomplete="list"></textarea>
|
|
2812
|
+
@if (mentionFor() === 'comment' && suggestions().length) {<ng-container *ngTemplateOutlet="mentionList; context: { el: cmtEl }" />}
|
|
2813
|
+
<div class="o-cf">
|
|
2814
|
+
@if (canEdit()) {<label for="att-in" class="o-lnk" style="color:var(--o-mut)">{{ uploading() ? 'Uploading…' : 'Attach files up to 25 MB' }}</label><input id="att-in" class="file-in" type="file" multiple (change)="upload($any($event.target))" data-testid="attach-input" />} @else {<span>Markdown</span>}
|
|
2815
|
+
<button type="button" class="o-btn o-sm o-dark o-r" (click)="addComment()" [disabled]="!draft().trim() || busy()" data-testid="comment-add">Comment</button>
|
|
2816
|
+
</div>
|
|
2506
2817
|
</div>
|
|
2507
2818
|
}
|
|
2508
2819
|
} @else {
|
|
@@ -2582,6 +2893,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2582
2893
|
<span class="o-faint">Created {{ shortDate(is.createdAt) }} by {{ people().name(is.reporterId) }}</span>
|
|
2583
2894
|
<span class="o-faint">Updated {{ ago(is.updatedAt) }}</span>
|
|
2584
2895
|
@if (is.resolvedAt) {<span class="o-faint">Resolved {{ shortDate(is.resolvedAt) }} · {{ is.resolution }}</span>}
|
|
2896
|
+
<span class="o-faint" data-testid="watchers">Watching: {{ watcherNames() }}</span>
|
|
2585
2897
|
</div>
|
|
2586
2898
|
@if (canManage()) {
|
|
2587
2899
|
@if (confirmDelete()) {
|
|
@@ -2602,7 +2914,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.12", ngImpo
|
|
|
2602
2914
|
} @else {
|
|
2603
2915
|
<div class="o-page"><div class="o-card"><div class="o-empty">Loading {{ key() }}…</div></div></div>
|
|
2604
2916
|
}
|
|
2605
|
-
|
|
2917
|
+
<ng-template #mentionList let-el="el">
|
|
2918
|
+
<ul class="mn-list" role="listbox" aria-label="People to mention">
|
|
2919
|
+
@for (p of suggestions(); track p.id; let i = $index) {
|
|
2920
|
+
<li role="option" [class.on]="i === mentionIndex()" [attr.aria-selected]="i === mentionIndex()" (mousedown)="$event.preventDefault(); pick(p, el)">
|
|
2921
|
+
<span class="o-av" aria-hidden="true">{{ people().initials(p.id) }}</span><span class="o-ell">{{ p.name || p.email }}</span>@if (p.email && p.name) {<span class="o-faint o-xs o-ell">{{ p.email }}</span>}
|
|
2922
|
+
</li>
|
|
2923
|
+
}
|
|
2924
|
+
</ul>
|
|
2925
|
+
</ng-template>
|
|
2926
|
+
`, styles: [".ti{font-size:24px;margin:0;line-height:1.25;text-wrap:pretty}.ti-btn{border:none;background:none;padding:0;text-align:left;font:inherit;color:inherit;cursor:text}.md :where(p,ul,ol,pre){margin:0}.md pre{background:var(--o-well);padding:10px 12px;border-radius:8px;overflow:auto;font-family:var(--o-mono);font-size:12px}.md code{font-family:var(--o-mono);font-size:12px;color:var(--o-tx);background:var(--o-sunken);padding:0 4px;border-radius:4px}.md pre code{background:none;padding:0}.md img{max-width:100%}.kv-in{width:100%}textarea.o-in{min-height:140px;font-family:var(--o-mono);font-size:12px;line-height:1.6;padding:10px 12px;height:auto}.md .mention{color:var(--o-acc-tx, var(--o-acc));background:var(--o-acc-bg, var(--o-sunken));border-radius:4px;padding:0 3px;font-weight:500}.rx{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;align-items:center}.rx .o-chip{cursor:pointer;font-size:12px}.rx .o-chip.mine{border-color:var(--o-acc);background:var(--o-acc-bg, var(--o-sunken))}.rx-pal{display:flex;gap:2px;padding:4px;border:1px solid var(--o-line);border-radius:8px;background:var(--o-surface);box-shadow:var(--o-shadow-sm, none)}.rx-pal button{border:none;background:none;font-size:16px;padding:2px 4px;border-radius:6px;cursor:pointer}.rx-pal button:hover,.rx-pal button:focus-visible{background:var(--o-sunken)}.mn{position:relative}.mn-list{position:absolute;left:8px;top:calc(100% - 4px);z-index:20;min-width:220px;max-width:320px;border:1px solid var(--o-line);border-radius:8px;background:var(--o-surface);box-shadow:var(--o-shadow, 0 6px 20px rgba(0,0,0,.12));padding:4px;list-style:none;margin:0}.mn-list li{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:13px}.mn-list li.on,.mn-list li:hover{background:var(--o-sunken)}.att-img{width:40px;height:40px;border-radius:6px;object-fit:cover;background:var(--o-well);flex:none;overflow:hidden;font-size:0}.att-ic{width:40px;height:40px;border-radius:6px;background:var(--o-well);display:grid;place-items:center;font-size:10px;color:var(--o-mut);font-family:var(--o-mono);flex:none;text-transform:uppercase}.file-in{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}\n"] }]
|
|
2606
2927
|
}], ctorParameters: () => [] });
|
|
2607
2928
|
|
|
2608
2929
|
/*
|