Skip to content

Commit 24b4536

Browse files
committed
Merge remote-tracking branch 'github/main'
2 parents ddeaf52 + d87fb5d commit 24b4536

22 files changed

+465
-66
lines changed

assets/css/tailwind.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ body {
127127
color: var(--color-foreground-contrast);
128128
}
129129

130+
::selection, mark {
131+
/* !important used to override selection color */
132+
background: var(--color-fill-yellow-iconic) !important;
133+
color: var(--color-foreground-black);
134+
}
135+
130136
@layer components {
131137
.button-solid {
132138
@apply flex h-8 cursor-pointer items-center justify-center gap-2 rounded-sm bg-rad-fill-secondary px-3 text-sm font-semibold text-rad-foreground-match-background hover:bg-rad-fill-secondary-hover;

components/Column.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,16 @@ const doneTasksFilter = doneTaskStatuses
135135
'hover:cursor-grab': !isDraggingDisabled && !isTaskDone(task),
136136
}"
137137
>
138-
<ColumnIssueCard v-if="isIssue(task)" :issue="task" />
139-
<ColumnPatchCard v-else-if="isPatch(task)" :patch="task" />
138+
<ColumnIssueCard
139+
v-if="isIssue(task)"
140+
:issue="task"
141+
:highlights="tasks.taskHighlights.get(task.id)"
142+
/>
143+
<ColumnPatchCard
144+
v-else-if="isPatch(task)"
145+
:patch="task"
146+
:highlights="tasks.taskHighlights.get(task.id)"
147+
/>
140148
</li>
141149

142150
<NewColumnIssueCard

components/ColumnCard.vue

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { taskTruncatedIdLength } from '~/constants/tasks'
3+
import type { TaskHighlights } from '~/types/tasks'
34
45
interface Props {
56
id: string
@@ -13,9 +14,39 @@ interface Props {
1314
}
1415
}
1516
href: string
17+
highlights?: TaskHighlights
1618
}
1719
1820
defineProps<Props>()
21+
22+
/**
23+
* Truncates segmented text based on a character limit.
24+
*
25+
* @param {string[]} segments - The segmented text to truncate.
26+
* @param {number} limit - The character limit.
27+
* @returns The truncated segmented text.
28+
*
29+
* @example
30+
* ```ts
31+
* truncateSegmentedText(['foo', 'bar'], 5) // ['foo', 'ba']
32+
* ```
33+
*/
34+
function truncateSegmentedText(segments: string[], limit: number): string[] {
35+
const truncatedText = []
36+
let currentLength = 0
37+
38+
for (const segment of segments) {
39+
if (currentLength + segment.length > limit) {
40+
truncatedText.push(segment.slice(0, limit - currentLength))
41+
break
42+
}
43+
44+
truncatedText.push(segment)
45+
currentLength += segment.length
46+
}
47+
48+
return truncatedText
49+
}
1950
</script>
2051

2152
<template>
@@ -27,24 +58,25 @@ defineProps<Props>()
2758
<UTooltip :text="status.name" :popper="{ placement: 'top' }">
2859
<Icon :name="status.icon.name" size="16" :class="status.icon.class" />
2960
</UTooltip>
30-
<pre class="text-xs font-medium text-rad-foreground-emphasized">{{
31-
id.slice(0, taskTruncatedIdLength)
32-
}}</pre>
61+
62+
<pre
63+
class="text-xs font-medium text-rad-foreground-emphasized"
64+
><TextWithHighlights :content="highlights?.id ? truncateSegmentedText(highlights?.id, taskTruncatedIdLength) : id.slice(0, taskTruncatedIdLength)" /></pre>
3365
</small>
3466

3567
<h4>
3668
<a :href="href" target="_blank" class="w-fit text-sm hover:underline">
37-
{{ title }}
69+
<TextWithHighlights :content="highlights?.title ?? title" />
3870
</a>
3971
</h4>
4072

4173
<ul v-if="labels.length > 0" class="flex flex-wrap gap-2">
4274
<li
43-
v-for="label in labels"
44-
:key="label"
75+
v-for="(label, index) in highlights?.labels ?? labels"
76+
:key="labels[index]"
4577
class="mt-2 rounded-full bg-rad-fill-ghost px-2 py-1 text-xs font-semibold text-rad-foreground-contrast group-hover:bg-rad-background-float"
4678
>
47-
{{ label }}
79+
<TextWithHighlights :content="label" />
4880
</li>
4981
</ul>
5082
</article>

components/ColumnIssueCard.vue

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script setup lang="ts">
22
import ColumnCard from './ColumnCard.vue'
3-
import type { Issue } from '~/types/tasks'
3+
import type { Issue, TaskHighlights } from '~/types/tasks'
44
55
const props = defineProps<{
66
issue: Issue
7+
highlights?: TaskHighlights
78
}>()
89
910
type ColumnCardStatus = InstanceType<typeof ColumnCard>['$props']['status']
@@ -23,12 +24,32 @@ const status = computed<ColumnCardStatus>(() => ({
2324
const radicleInterfaceBaseUrl = useRadicleInterfaceBaseUrl()
2425
const isDebugging = useIsDebugging()
2526
27+
// TODO: zac reduce duplication between ColumnIssueCard and ColumnPatchCard
2628
const labels = computed(() =>
2729
isDebugging.value
2830
? props.issue.labels
2931
: props.issue.labels.filter((label) => !label.startsWith(dataLabelNamespace)),
3032
)
3133
34+
const highlights = computed(() => {
35+
if (!props.highlights) {
36+
return undefined
37+
}
38+
39+
if (isDebugging.value) {
40+
return props.highlights
41+
}
42+
43+
const filteredHighlightLabels = props.highlights.labels.filter(
44+
(label) => !(label[0] ?? '').startsWith(dataLabelNamespace),
45+
)
46+
47+
return {
48+
...props.highlights,
49+
labels: filteredHighlightLabels,
50+
}
51+
})
52+
3253
const href = computed(() =>
3354
new URL(
3455
`/nodes/${route.params.node}/${route.params.rid}/issues/${props.issue.id}`,
@@ -45,5 +66,6 @@ const href = computed(() =>
4566
:labels="labels"
4667
:href="href"
4768
:status="status"
69+
:highlights="highlights"
4870
/>
4971
</template>

components/ColumnPatchCard.vue

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
<script setup lang="ts">
22
import ColumnCard from './ColumnCard.vue'
3-
import type { Patch } from '~/types/tasks'
3+
import type { Patch, TaskHighlights } from '~/types/tasks'
44
55
const props = defineProps<{
66
patch: Patch
7+
highlights?: TaskHighlights
78
}>()
89
910
type ColumnCardStatus = InstanceType<typeof ColumnCard>['$props']['status']
@@ -28,12 +29,32 @@ const status = computed<ColumnCardStatus>(() => ({
2829
const radicleInterfaceBaseUrl = useRadicleInterfaceBaseUrl()
2930
const isDebugging = useIsDebugging()
3031
32+
// TODO: zac reduce duplication between ColumnIssueCard and ColumnPatchCard
3133
const labels = computed(() =>
3234
isDebugging.value
3335
? props.patch.labels
3436
: props.patch.labels.filter((label) => !label.startsWith(dataLabelNamespace)),
3537
)
3638
39+
const highlights = computed(() => {
40+
if (!props.highlights) {
41+
return undefined
42+
}
43+
44+
if (isDebugging.value) {
45+
return props.highlights
46+
}
47+
48+
const filteredHighlightLabels = props.highlights.labels.filter(
49+
(label) => !(label[0] ?? '').startsWith(dataLabelNamespace),
50+
)
51+
52+
return {
53+
...props.highlights,
54+
labels: filteredHighlightLabels,
55+
}
56+
})
57+
3758
const href = computed(() =>
3859
new URL(
3960
`/nodes/${route.params.node}/${route.params.rid}/patches/${props.patch.id}`,
@@ -50,5 +71,6 @@ const href = computed(() =>
5071
:labels="labels"
5172
:href="href"
5273
:status="status"
74+
:highlights="highlights"
5375
/>
5476
</template>

components/Header.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ async function handleImport() {
4040
</script>
4141

4242
<template>
43-
<header class="flex justify-between gap-4 px-4">
44-
<TaskKindSelect />
43+
<header class="flex justify-between gap-4 p-4">
44+
<div class="flex gap-4">
45+
<TaskKindSelect />
46+
<TaskFilterInput />
47+
</div>
4548

4649
<div class="flex gap-4">
4750
<a

components/IconButton.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
defineProps<{
33
label: string
44
icon: string
5+
padded?: boolean
56
}>()
67
</script>
78

89
<template>
910
<button
1011
type="button"
11-
class="w-fit rounded p-1 leading-none text-rad-foreground-dim hover:bg-rad-fill-ghost disabled:bg-transparent disabled:text-rad-foreground-disabled"
12+
class="w-fit cursor-pointer rounded leading-none text-rad-foreground-dim hover:bg-rad-fill-ghost disabled:cursor-none disabled:bg-transparent disabled:text-rad-foreground-disabled"
13+
:class="{ 'p-1': padded ?? true }"
1214
>
1315
<span class="sr-only">{{ label }}</span>
1416
<Icon :name="icon" size="20" />

components/TaskFilterInput.vue

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<script setup lang="ts">
2+
import { elementIds } from '~/constants/elements'
3+
4+
defineOptions({ inheritAttrs: false })
5+
6+
const { queryParams } = useQueryParamsStore()
7+
8+
const inputRef = ref<HTMLInputElement>()
9+
const { focused } = useFocus(inputRef)
10+
// TODO: zac disable this while a modal is open
11+
useEventListener('keydown', (event) => {
12+
const ctrlF = (event.ctrlKey || event.metaKey) && event.code === 'KeyF'
13+
if (!ctrlF) {
14+
return
15+
}
16+
17+
event.preventDefault()
18+
inputRef.value?.focus()
19+
})
20+
21+
// TODO: zac disable this while a modal is open
22+
onKeyStroke('Escape', (event) => {
23+
if (!focused) {
24+
return
25+
}
26+
27+
event.preventDefault()
28+
inputRef.value?.blur()
29+
})
30+
31+
const board = useBoardStore()
32+
const label = computed(() => {
33+
switch (board.state.filter.taskKind) {
34+
case 'issue':
35+
return 'Filter issues'
36+
case 'patch':
37+
return 'Filter patches'
38+
default:
39+
return 'Filter'
40+
}
41+
})
42+
43+
// Prevents blur when clicking outside the input element
44+
function handleMouseDown(event: MouseEvent) {
45+
if (event.target !== inputRef.value) {
46+
event.preventDefault()
47+
}
48+
}
49+
50+
// TODO: zac pull this out into a generic utility file if reused
51+
function isMac() {
52+
// TODO: zac look into non-deprecated alternative
53+
return navigator.platform.toLowerCase().includes('mac')
54+
}
55+
const filterShortcut = `${isMac() ? 'Cmd' : 'Ctrl'}+F`
56+
</script>
57+
58+
<template>
59+
<form
60+
:id="elementIds.taskFilterForm"
61+
role="search"
62+
class="flex w-96 cursor-text items-center gap-2 rounded-sm border border-rad-border-hint bg-rad-background-dip px-2 text-sm focus-within:ring-2 focus-within:ring-rad-fill-secondary hover:border-rad-border-default"
63+
@click="inputRef?.focus()"
64+
@mousedown="handleMouseDown"
65+
@submit.prevent
66+
>
67+
<Icon name="octicon:filter-16" class="text-rad-foreground-dim" size="16" />
68+
<input
69+
v-bind="$attrs"
70+
ref="inputRef"
71+
v-model="queryParams.filter"
72+
type="text"
73+
class="h-full flex-1 bg-inherit outline-none"
74+
:aria-label="label"
75+
:placeholder="`${label}…`"
76+
/>
77+
<UTooltip v-show="Boolean(queryParams.filter)" text="Clear">
78+
<IconButton
79+
label="Clear"
80+
icon="octicon:x-16"
81+
:padded="false"
82+
@click="queryParams.filter = ''"
83+
/>
84+
</UTooltip>
85+
<!-- TODO: zac hide on mobile devices -->
86+
<UTooltip v-show="!focused" :text="`Press ${filterShortcut} to focus`">
87+
<kbd
88+
class="flex h-5 min-w-5 select-none items-center justify-center rounded-sm border border-rad-border-default px-1 font-sans text-xs font-medium"
89+
>
90+
{{ filterShortcut }}
91+
</kbd>
92+
</UTooltip>
93+
</form>
94+
</template>

components/TextWithHighlights.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script setup lang="ts">
2+
interface Props {
3+
content: string | string[]
4+
}
5+
6+
defineProps<Props>()
7+
</script>
8+
9+
10+
<template>
11+
<template v-if="typeof content === 'string'">
12+
{{ content }}
13+
</template>
14+
<template v-for="(segment, index) in content" v-else :key="index">
15+
<template v-if="isEven(index)">{{ segment }}</template>
16+
<mark v-else>{{ segment }}</mark>
17+
</template>
18+
</template>

0 commit comments

Comments
 (0)