Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .run/Docker Compose Up.run.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Docker Compose Up" type="ShConfigurationType" factoryName="Shell Script">
<option name="SCRIPT_TEXT" value="docker compose up -d" />
<configuration default="false" name="Docker Compose Up" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="docker compose up --wait" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
</component>
</component>
1 change: 0 additions & 1 deletion .run/Spring Development.run.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<option name="SPRING_BOOT_MAIN_CLASS" value="codes.yam.contacts.ContactsApplication" />
<method v="2">
<option name="Make" enabled="true" />
<option name="ToolBeforeRunTask" enabled="true" actionId="Tool_External Tools_Docker Compose Up + Wait" />
</method>
</configuration>
</component>
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ spring:
jpa:
hibernate:
ddl-auto: validate
sql:
init:
mode: always
thymeleaf:
cache: false
prefix: file:src/main/resources/templates/
Expand Down
29 changes: 11 additions & 18 deletions src/main/resources/static/css/input.css
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,13 @@
overflow-y: auto;
max-height: calc(100vh - 11rem);
}
.contacts-table th:nth-child(1),
.contacts-table td:nth-child(1) { width: 2.5rem; }
.contacts-table td:nth-child(3) { width: 9rem; }
.contacts-table td:nth-child(4) { width: 5rem; }

/* Table header selected state */
.thead-selected {
background-color: oklch(47.3% 0.217 264.4); /* blue-600 */
}
.thead-selected,
.thead-selected * {
color: white;
}
.thead-selected .btn-ghost:hover {
background-color: oklch(55% 0.217 264.4);
.contacts-table td:nth-child(2) { width: 9rem; }
.contacts-table td:nth-child(3) { width: 5rem; }

/* Selection accent colors */
:root {
--color-selection: oklch(47.3% 0.217 264.4);
--color-selection-bold: oklch(55% 0.2 260);
}

/* Unlayered - beats all @layer styles */
Expand All @@ -157,8 +149,9 @@
transition: none;
animation: none;
}
.checkbox:checked {
background-color: oklch(55% 0.2 260);
border-color: oklch(55% 0.2 260);
.checkbox:checked,
.checkbox:indeterminate {
background-color: var(--color-selection-bold);
border-color: var(--color-selection-bold);
color: white;
}
97 changes: 79 additions & 18 deletions src/main/resources/templates/contacts/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,44 +12,105 @@
<div layout:fragment="content">
<!-- Table -->
<form
th:attr="hx-delete=@{/contacts}"
hx-target="body"
hx-push-url="true"
hx-confirm="Delete selected contacts? This cannot be undone."
x-data="{ selected: 0 }"
x-init="selected = $el.querySelectorAll('input[name=selected_contact_slugs]:checked').length"
hx-push-url="true"
hx-target="body"
th:attr="hx-delete=@{/contacts}"
x-data="{
selected: 0,
total: 0,
syncSelectAll() {
$refs.selectAll.indeterminate = this.selected > 0 && this.selected < this.total;
$refs.selectAll.checked = this.selected === this.total && this.total > 0;
},
selectAll() {
this.$root.querySelectorAll('input[name=selected_contact_slugs]').forEach(cb => cb.checked = true);
this.selected = this.total;
},
deselectAll() {
this.$root.querySelectorAll('input[name=selected_contact_slugs]').forEach(cb => cb.checked = false);
this.selected = 0;
},
refreshCounts() {
this.total = this.$el.querySelectorAll('input[name=selected_contact_slugs]').length;
this.selected = this.$el.querySelectorAll('input[name=selected_contact_slugs]:checked').length;
this.syncSelectAll();
},
}"
x-init="
selected = $el.querySelectorAll('input[name=selected_contact_slugs]:checked').length;
total = $el.querySelectorAll('input[name=selected_contact_slugs]').length;
$el.addEventListener('htmx:afterSwap', () => refreshCounts());
"
>
<div class="bg-base-100 border-base-300 overflow-hidden rounded-lg border">
<table class="contacts-table table">
<thead>
<tr :class="selected > 0 ? 'thead-selected' : ''">
<th class="w-10"><!-- TODO: select-all checkbox --></th>
<tr :class="selected > 0 ? 'text-primary' : ''">
<th colspan="3">
<div class="flex items-center justify-between">
<button type="submit" class="btn btn-ghost btn-xs gap-1" :class="selected === 0 ? 'invisible' : ''">
<i data-lucide="trash-2" class="h-3.5 w-3.5"></i>
<span>Delete (<span x-text="selected"></span>)</span>
</button>
<div class="flex items-center pl-1.5">
<div class="flex items-center">
<label aria-label="Select all">
<input
@change="selected > 0 ? deselectAll() : selectAll()"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Select-all toggle condition is wrong for partial selection.

At Line 47, any selected > 0 triggers deselectAll(). In indeterminate state, clicking the header checkbox should select all, not clear all.

Suggested fix
-@change="selected > 0 ? deselectAll() : selectAll()"
+@change="selected === total ? deselectAll() : selectAll()"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@change="selected > 0 ? deselectAll() : selectAll()"
`@change`="selected === total ? deselectAll() : selectAll()"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/resources/templates/contacts/index.html` at line 47, The header
checkbox currently uses `@change`="selected > 0 ? deselectAll() : selectAll()",
which treats the indeterminate state as "deselect all"; change the condition to
only deselect when everything is already selected (e.g., selected ===
items.length or selected === contacts.length), otherwise call selectAll() so
clicking in the indeterminate state selects all; update the `@change` handler to
reference the collection size instead of just selected > 0 while keeping the
existing deselectAll() and selectAll() calls.

@click.stop
class="checkbox checkbox-sm"
type="checkbox"
x-effect="syncSelectAll()"
x-ref="selectAll"
/>
</label>
<div class="dropdown">
<button class="btn btn-ghost btn-xs px-0" tabindex="0" type="button">
<i class="h-3 w-3" data-lucide="chevron-down"></i>
</button>
<ul
@click="document.activeElement.blur()"
class="dropdown-content bg-base-100 rounded-box z-10 w-32 p-2 font-normal shadow"
tabindex="0"
>
<li @click="selectAll()" class="hover:bg-base-200 cursor-pointer rounded px-2 py-1">All</li>
<li @click="deselectAll()" class="hover:bg-base-200 cursor-pointer rounded px-2 py-1">
None
</li>
</ul>
</div>
</div>
<button
:class="selected === 0 ? 'invisible' : ''"
:style="selected > 0 ? 'color: var(--color-selection)' : ''"
class="btn btn-ghost btn-xs gap-1"
type="submit"
>
<i class="h-3.5 w-3.5" data-lucide="trash-2"></i>
<span>Delete (<span x-text="selected"></span>)</span>
</button>
</div>
<div class="flex items-center gap-1">
<!-- TODO: refresh icon -->
<div class="flex items-center gap-1">
<span
class="text-base-content/50 px-1 text-xs"
:class="selected > 0 ? '' : 'text-base-content/50'"
:style="selected > 0 ? 'color: var(--color-selection)' : ''"
class="px-1 text-xs"
th:text="${contactPage.number * contactPage.size + 1} + '–' + ${contactPage.isLast() ? contactPage.totalElements : (contactPage.number + 1) * contactPage.size} + ' of ' + ${#numbers.formatInteger(contactPage.totalElements, 0, 'COMMA')}"
></span>
<a
th:href="@{/contacts(page=${contactPage.number - 1}, q=${search}, sort=${sort}, size=${contactPage.size})}"
th:classappend="${contactPage.isFirst()} ? 'opacity-30 pointer-events-none' : ''"
:style="selected > 0 ? 'color: var(--color-selection)' : ''"
class="btn btn-ghost btn-xs"
th:classappend="${contactPage.isFirst()} ? 'opacity-30 pointer-events-none' : ''"
th:href="@{/contacts(page=${contactPage.number - 1}, q=${search}, sort=${sort}, size=${contactPage.size})}"
>
<i data-lucide="chevron-left" class="h-3.5 w-3.5"></i>
<i class="h-3.5 w-3.5" data-lucide="chevron-left"></i>
</a>
<a
th:href="@{/contacts(page=${contactPage.number + 1}, q=${search}, sort=${sort}, size=${contactPage.size})}"
th:classappend="${contactPage.isLast()} ? 'opacity-30 pointer-events-none' : ''"
:style="selected > 0 ? 'color: var(--color-selection)' : ''"
class="btn btn-ghost btn-xs"
th:classappend="${contactPage.isLast()} ? 'opacity-30 pointer-events-none' : ''"
th:href="@{/contacts(page=${contactPage.number + 1}, q=${search}, sort=${sort}, size=${contactPage.size})}"
>
<i data-lucide="chevron-right" class="h-3.5 w-3.5"></i>
<i class="h-3.5 w-3.5" data-lucide="chevron-right"></i>
</a>
</div>
</div>
Expand Down
41 changes: 23 additions & 18 deletions src/main/resources/templates/fragments/contact-list-rows.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,34 @@
<tr
th:each="contact : ${contacts}"
th:object="${contact}"
class="hover:bg-base-300 group cursor-pointer"
class="hover:bg-base-300 group/row cursor-pointer"
th:onclick="|window.location='@{/contacts/{slug}(slug=*{slug})}'|"
>
<th @click.stop>
<label>
<!--suppress JSUnresolvedReference it's fine -->
<input
name="selected_contact_slugs"
th:attr="aria-label=*{first} + ' ' + *{last}"
th:value="*{slug}"
type="checkbox"
class="checkbox checkbox-sm"
@change="selected += $event.target.checked ? 1 : -1"
/>
</label>
</th>
<td>
<div class="flex items-center gap-3">
<div
class="bg-base-200 text-base-content/60 flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-xs font-semibold"
th:text="*{#strings.substring(first, 0, 1) + #strings.substring(last, 0, 1)}"
class="relative h-8 w-8 shrink-0 [&:has(:checked)>div]:hidden [&:has(:checked)>label]:pointer-events-auto [&:has(:checked)>label]:opacity-100"
>
AB
<div
class="bg-base-200 text-base-content/60 absolute inset-0 flex items-center justify-center rounded-full text-xs font-semibold group-hover/row:hidden"
th:text="*{#strings.substring(first, 0, 1) + #strings.substring(last, 0, 1)}"
>
AB
</div>
<!--suppress JSUnresolvedReference it's fine -->
<label
class="pointer-events-none absolute inset-0 flex cursor-pointer items-center justify-center opacity-0 group-hover/row:pointer-events-auto group-hover/row:opacity-100"
@click.stop
>
<input
name="selected_contact_slugs"
th:attr="aria-label=*{first} + ' ' + *{last}"
th:value="*{slug}"
type="checkbox"
class="checkbox checkbox-sm"
@change="selected += $event.target.checked ? 1 : -1"
/>
</label>
</div>
<div>
<div class="text-base-content font-medium" th:text="*{first + ' ' + last}"></div>
Expand All @@ -54,7 +59,7 @@
</td>
<td class="text-base-content/50 font-mono text-xs" th:text="*{phone}"></td>
<td @click.stop>
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100" id="actions">
<div class="actions flex items-center justify-end gap-1 opacity-0 group-hover/row:opacity-100">
<a
th:href="@{/contacts/{slug}/edit(slug=*{slug})}"
th:attr="aria-label='Edit ' + *{first} + ' ' + *{last}"
Expand Down
6 changes: 6 additions & 0 deletions src/main/resources/templates/layout.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
document.documentElement.setAttribute("data-theme", "shadcn-dark");
</script>
<script src="https://unpkg.com/lucide@0.577.0/dist/umd/lucide.min.js"></script>
<meta name="_csrf" th:content="${_csrf.token}" />
<meta name="_csrf_header" th:content="${_csrf.headerName}" />
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.15.8/dist/cdn.min.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.8/dist/cdn.min.js" defer></script>
Expand Down Expand Up @@ -92,6 +94,10 @@
lucide.createIcons();
//noinspection JSUnresolvedVariable
document.addEventListener("htmx:afterSettle", (e) => lucide.createIcons({ root: e.detail.elt }));
document.addEventListener("htmx:configRequest", (e) => {
e.detail.headers[document.querySelector("meta[name=_csrf_header]").content] =
document.querySelector("meta[name=_csrf]").content;
});
</script>
</body>
</html>
Loading