CODEX - Moar Changes
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useRef, useMemo } from "react";
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import api from "../api/client";
|
import api from "../api/client";
|
||||||
import { useAuth } from "../auth/AuthContext";
|
import { useAuth } from "../auth/AuthContext";
|
||||||
@@ -14,6 +14,12 @@ import {
|
|||||||
const MELODY_TYPES = ["", "orthodox", "catholic", "all"];
|
const MELODY_TYPES = ["", "orthodox", "catholic", "all"];
|
||||||
const MELODY_TONES = ["", "normal", "festive", "cheerful", "lamentation"];
|
const MELODY_TONES = ["", "normal", "festive", "cheerful", "lamentation"];
|
||||||
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";
|
const NOTE_LABELS = "ABCDEFGHIJKLMNOP";
|
||||||
|
const SORTABLE_COLUMN_TO_KEY = {
|
||||||
|
name: "name",
|
||||||
|
totalActiveBells: "totalActiveBells",
|
||||||
|
dateCreated: "dateCreated",
|
||||||
|
dateEdited: "dateEdited",
|
||||||
|
};
|
||||||
|
|
||||||
// All available columns with their defaults
|
// All available columns with their defaults
|
||||||
const ALL_COLUMNS = [
|
const ALL_COLUMNS = [
|
||||||
@@ -271,7 +277,7 @@ export default function MelodyList() {
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case "name":
|
case "name":
|
||||||
return getDisplayName(info.name).toLowerCase();
|
return getDisplayName(info.name).toLowerCase();
|
||||||
case "totalBells":
|
case "totalActiveBells":
|
||||||
return Number(info.totalActiveBells || 0);
|
return Number(info.totalActiveBells || 0);
|
||||||
case "dateEdited":
|
case "dateEdited":
|
||||||
return parseDateValue(metadata.dateEdited);
|
return parseDateValue(metadata.dateEdited);
|
||||||
@@ -300,6 +306,24 @@ export default function MelodyList() {
|
|||||||
});
|
});
|
||||||
}, [melodies, createdByFilter, sortBy, sortDir]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [melodies, createdByFilter, sortBy, sortDir]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const handleSortClick = (columnKey) => {
|
||||||
|
const nextSortKey = SORTABLE_COLUMN_TO_KEY[columnKey];
|
||||||
|
if (!nextSortKey) return;
|
||||||
|
if (sortBy === nextSortKey) {
|
||||||
|
setSortDir((prev) => (prev === "asc" ? "desc" : "asc"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSortBy(nextSortKey);
|
||||||
|
setSortDir(nextSortKey === "dateCreated" || nextSortKey === "dateEdited" ? "desc" : "asc");
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSortIndicator = (columnKey) => {
|
||||||
|
const sortKey = SORTABLE_COLUMN_TO_KEY[columnKey];
|
||||||
|
if (!sortKey) return null;
|
||||||
|
if (sortBy !== sortKey) return "↕";
|
||||||
|
return sortDir === "asc" ? "▲" : "▼";
|
||||||
|
};
|
||||||
|
|
||||||
const renderCellValue = (key, row) => {
|
const renderCellValue = (key, row) => {
|
||||||
const info = row.information || {};
|
const info = row.information || {};
|
||||||
const ds = row.default_settings || {};
|
const ds = row.default_settings || {};
|
||||||
@@ -398,7 +422,44 @@ export default function MelodyList() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case "duration":
|
case "duration":
|
||||||
return ds.duration != null ? formatDuration(ds.duration) : "-";
|
if (ds.duration == null) return "-";
|
||||||
|
if (Number(ds.duration) === 0) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="px-2 py-0.5 text-xs rounded-full"
|
||||||
|
style={{ backgroundColor: "rgba(88,156,250,0.18)", color: "var(--text-link)" }}
|
||||||
|
>
|
||||||
|
Single Run
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const allDurationValues = melodySettings?.duration_values || [];
|
||||||
|
const nonZeroDurations = allDurationValues.filter((v) => Number(v) > 0);
|
||||||
|
const idx = nonZeroDurations.indexOf(Number(ds.duration));
|
||||||
|
const percent = idx >= 0 && nonZeroDurations.length > 0
|
||||||
|
? Math.round(((idx + 1) / nonZeroDurations.length) * 100)
|
||||||
|
: 0;
|
||||||
|
return (
|
||||||
|
<div className="min-w-28">
|
||||||
|
<div className="text-xs mb-1" style={{ color: "var(--text-secondary)" }}>
|
||||||
|
{formatDuration(ds.duration)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="w-full h-2 rounded-full"
|
||||||
|
style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)" }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full transition-all"
|
||||||
|
style={{
|
||||||
|
width: `${Math.max(0, Math.min(100, percent))}%`,
|
||||||
|
backgroundColor: speedBarColor(percent),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
case "totalRunDuration":
|
case "totalRunDuration":
|
||||||
return ds.totalRunDuration ?? "-";
|
return ds.totalRunDuration ?? "-";
|
||||||
case "pauseDuration":
|
case "pauseDuration":
|
||||||
@@ -417,14 +478,14 @@ export default function MelodyList() {
|
|||||||
);
|
);
|
||||||
case "noteAssignments":
|
case "noteAssignments":
|
||||||
return ds.noteAssignments?.length > 0 ? (
|
return ds.noteAssignments?.length > 0 ? (
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="flex flex-nowrap gap-1 whitespace-nowrap">
|
||||||
{ds.noteAssignments.map((assignedBell, noteIdx) => (
|
{ds.noteAssignments.map((assignedBell, noteIdx) => (
|
||||||
<div
|
<div
|
||||||
key={noteIdx}
|
key={noteIdx}
|
||||||
className="flex flex-col items-center rounded-md border"
|
className="flex flex-col items-center rounded-md border"
|
||||||
style={{
|
style={{
|
||||||
minWidth: "30px",
|
minWidth: "26px",
|
||||||
padding: "3px 5px",
|
padding: "3px 3px",
|
||||||
backgroundColor: "var(--bg-card-hover)",
|
backgroundColor: "var(--bg-card-hover)",
|
||||||
borderColor: "var(--border-primary)",
|
borderColor: "var(--border-primary)",
|
||||||
}}
|
}}
|
||||||
@@ -434,7 +495,7 @@ export default function MelodyList() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="w-full my-0.5" style={{ height: "1px", backgroundColor: "var(--border-primary)" }} />
|
<div className="w-full my-0.5" style={{ height: "1px", backgroundColor: "var(--border-primary)" }} />
|
||||||
<span className="text-xs leading-tight" style={{ color: "var(--text-muted)" }}>
|
<span className="text-xs leading-tight" style={{ color: "var(--text-muted)" }}>
|
||||||
{assignedBell > 0 ? assignedBell : "—"}
|
{assignedBell > 0 ? assignedBell : "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -551,26 +612,6 @@ export default function MelodyList() {
|
|||||||
<option value="draft">Drafts</option>
|
<option value="draft">Drafts</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select
|
|
||||||
value={sortBy}
|
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
|
||||||
className={selectClass}
|
|
||||||
>
|
|
||||||
<option value="dateCreated">Sort: Date Created</option>
|
|
||||||
<option value="dateEdited">Sort: Date Edited</option>
|
|
||||||
<option value="name">Sort: Name</option>
|
|
||||||
<option value="totalBells">Sort: Total Bells</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select
|
|
||||||
value={sortDir}
|
|
||||||
onChange={(e) => setSortDir(e.target.value)}
|
|
||||||
className={selectClass}
|
|
||||||
>
|
|
||||||
<option value="desc">Desc</option>
|
|
||||||
<option value="asc">Asc</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div className="relative" ref={creatorPickerRef}>
|
<div className="relative" ref={creatorPickerRef}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -724,10 +765,33 @@ export default function MelodyList() {
|
|||||||
{activeColumns.map((col) => (
|
{activeColumns.map((col) => (
|
||||||
<th
|
<th
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className={`px-4 py-3 text-left font-medium ${col.key === "color" ? "w-8 px-2" : ""}`}
|
className={`px-4 py-3 text-left font-medium ${
|
||||||
|
col.key === "color" ? "w-8 px-2" : col.key === "noteAssignments" ? "min-w-[420px]" : ""
|
||||||
|
}`}
|
||||||
style={{ color: "var(--text-muted)" }}
|
style={{ color: "var(--text-muted)" }}
|
||||||
>
|
>
|
||||||
{col.key === "color" ? "" : col.label}
|
{col.key === "color" ? "" : (
|
||||||
|
<div className="inline-flex items-center gap-1">
|
||||||
|
<span>{col.label}</span>
|
||||||
|
{SORTABLE_COLUMN_TO_KEY[col.key] ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSortClick(col.key);
|
||||||
|
}}
|
||||||
|
className="text-xs leading-none px-1 rounded cursor-pointer"
|
||||||
|
style={{
|
||||||
|
color: sortBy === SORTABLE_COLUMN_TO_KEY[col.key] ? "var(--accent)" : "var(--text-muted)",
|
||||||
|
backgroundColor: sortBy === SORTABLE_COLUMN_TO_KEY[col.key] ? "rgba(116,184,22,0.14)" : "transparent",
|
||||||
|
}}
|
||||||
|
title={`Sort by ${col.label}`}
|
||||||
|
>
|
||||||
|
{getSortIndicator(col.key)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
@@ -748,7 +812,9 @@ export default function MelodyList() {
|
|||||||
{activeColumns.map((col) => (
|
{activeColumns.map((col) => (
|
||||||
<td
|
<td
|
||||||
key={col.key}
|
key={col.key}
|
||||||
className={`px-4 py-3 ${col.key === "color" ? "w-8 px-2" : ""}`}
|
className={`px-4 py-3 ${
|
||||||
|
col.key === "color" ? "w-8 px-2" : col.key === "noteAssignments" ? "min-w-[420px]" : ""
|
||||||
|
}`}
|
||||||
style={{ color: "var(--text-primary)" }}
|
style={{ color: "var(--text-primary)" }}
|
||||||
>
|
>
|
||||||
{renderCellValue(col.key, row)}
|
{renderCellValue(col.key, row)}
|
||||||
@@ -819,3 +885,4 @@ export default function MelodyList() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user