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
6 changes: 3 additions & 3 deletions app/Models/Membership.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
namespace App\Models;

use App\Enums\OrganizationRole;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Carbon;

/**
* @property int $id
* @property int $organization_id
* @property int $user_id
* @property OrganizationRole $role
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
* @property-read Organization $organization
* @property-read User $user
*/
Expand Down
6 changes: 3 additions & 3 deletions app/Models/OrderStatusSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace App\Models;

use Carbon\CarbonImmutable;
use Database\Factories\OrderStatusSettingFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;

/**
* What an organization has decided one WooCommerce order status means.
Expand All @@ -22,8 +22,8 @@
* @property string $status
* @property string|null $label
* @property bool $counts_as_revenue
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
* @property-read Organization $organization
*/
#[Fillable(['status', 'label', 'counts_as_revenue'])]
Expand Down
8 changes: 4 additions & 4 deletions app/Models/Organization.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Concerns\GeneratesUniqueOrganizationSlugs;
use App\Enums\OrganizationRole;
use Carbon\CarbonImmutable;
use Database\Factories\OrganizationFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Collection;
Expand All @@ -13,16 +14,15 @@
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;

/**
* @property int $id
* @property string $name
* @property string $slug
* @property string|null $timezone
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Carbon|null $deleted_at
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
* @property CarbonImmutable|null $deleted_at
* @property-read Collection<int, OrganizationInvitation> $invitations
* @property-read Collection<int, Membership> $memberships
* @property-read Collection<int, User> $members
Expand Down
10 changes: 5 additions & 5 deletions app/Models/OrganizationInvitation.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
namespace App\Models;

use App\Enums\OrganizationRole;
use Carbon\CarbonImmutable;
use Database\Factories\OrganizationInvitationFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;

/**
Expand All @@ -19,10 +19,10 @@
* @property string $email
* @property OrganizationRole $role
* @property int $invited_by
* @property Carbon|null $expires_at
* @property Carbon|null $accepted_at
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property CarbonImmutable|null $expires_at
* @property CarbonImmutable|null $accepted_at
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
* @property-read Organization $organization
* @property-read User $inviter
*/
Expand Down
8 changes: 4 additions & 4 deletions app/Models/Shop.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use App\Data\ShopConnectionResult;
use App\Enums\ShopConnectionStatus;
use App\Enums\ShopPlatform;
use Carbon\CarbonImmutable;
use Database\Factories\ShopFactory;
use Illuminate\Database\Eloquent\Attributes\Fillable;
use Illuminate\Database\Eloquent\Attributes\Hidden;
Expand All @@ -15,7 +16,6 @@
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;

/**
Expand All @@ -29,10 +29,10 @@
* @property string $consumer_secret
* @property ShopConnectionStatus $connection_status
* @property string|null $connection_message
* @property Carbon|null $connection_checked_at
* @property CarbonImmutable|null $connection_checked_at
* @property int|null $connection_response_time_ms
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
* @property-read Organization $organization
* @property-read int|null $orders_count
* @property-read ShopSyncState|null $syncState
Expand Down
10 changes: 5 additions & 5 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Models;

use App\Concerns\HasOrganizations;
use Carbon\CarbonImmutable;
use Database\Factories\UserFactory;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
Expand All @@ -13,7 +14,6 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Laravel\Fortify\Contracts\PasskeyUser;
use Laravel\Fortify\PasskeyAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
Expand All @@ -22,15 +22,15 @@
* @property int $id
* @property string $name
* @property string $email
* @property Carbon|null $email_verified_at
* @property CarbonImmutable|null $email_verified_at
* @property string $password
* @property string|null $two_factor_secret
* @property string|null $two_factor_recovery_codes
* @property Carbon|null $two_factor_confirmed_at
* @property CarbonImmutable|null $two_factor_confirmed_at
* @property string|null $remember_token
* @property int|null $current_organization_id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property CarbonImmutable|null $created_at
* @property CarbonImmutable|null $updated_at
* @property-read Organization|null $currentOrganization
* @property-read Collection<int, Organization> $ownedOrganizations
* @property-read Collection<int, Membership> $organizationMemberships
Expand Down
52 changes: 52 additions & 0 deletions resources/js/components/pagination-arrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Link } from '@inertiajs/react';
import type { LucideIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';

/**
* One step of a paginated list, as an icon button.
*
* The paginator hands back a null url at either end, which is the disabled
* state: the control stays in place so the row of arrows does not reflow on
* the first and last page.
*/
export default function PaginationArrow({
href,
label,
icon: Icon,
test,
}: {
href: string | null;
label: string;
icon: LucideIcon;
test: string;
}) {
if (!href) {
return (
<Button
variant="ghost"
size="icon"
className="size-8"
data-test={test}
disabled
>
<Icon />
<span className="sr-only">{label}</span>
</Button>
);
}

return (
<Button
variant="ghost"
size="icon"
className="size-8"
data-test={test}
asChild
>
<Link href={href} preserveScroll preserveState>
<Icon />
<span className="sr-only">{label}</span>
</Link>
</Button>
);
}
19 changes: 19 additions & 0 deletions resources/js/hooks/use-debounced-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from 'react';

/**
* Hold back a rapidly changing value until it settles.
*
* The list filters use this so a request goes out once the typing stops
* rather than once per keystroke.
*/
export function useDebouncedValue<T>(value: T, delay = 300): T {
const [debounced, setDebounced] = useState(value);

useEffect(() => {
const timeout = window.setTimeout(() => setDebounced(value), delay);

return () => window.clearTimeout(timeout);
}, [value, delay]);

return debounced;
}
84 changes: 31 additions & 53 deletions resources/js/pages/shops/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Head, Link, router, usePage } from '@inertiajs/react';
import type { LucideIcon } from 'lucide-react';
import {
ChevronLeft,
ChevronRight,
Expand All @@ -15,14 +14,16 @@ import {
TriangleAlert,
X,
} from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useState } from 'react';
import DeleteShopModal from '@/components/delete-shop-modal';
import ShopFormModal from '@/components/shop-form-modal';
import StatusBadge, {
statusToneText,
StatusTooltip,
} from '@/components/status-badge';
import { Badge } from '@/components/ui/badge';
import PaginationArrow from '@/components/pagination-arrow';
import { useDebouncedValue } from '@/hooks/use-debounced-value';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
Expand Down Expand Up @@ -76,13 +77,34 @@ export default function ShopsIndex({
const [deleteOpen, setDeleteOpen] = useState(false);
const [shopToDelete, setShopToDelete] = useState<Shop | null>(null);
const [testingShopId, setTestingShopId] = useState<number | null>(null);
const searchTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
const debouncedSearch = useDebouncedValue(search);

const applySearch = (value: string) => {
if (!currentOrganization) {
return;
}

router.get(
shopsIndex(currentOrganization.slug).url,
value ? { search: value } : {},
{ preserveState: true, preserveScroll: true, replace: true },
);
};

/**
* The server value is the source of truth, so a visit only fires when the
* settled input disagrees with it. That skips the pointless request on
* mount and converges rather than looping once our own response lands.
*/
useEffect(() => {
const timeout = searchTimeout;
const term = debouncedSearch.trim();

return () => clearTimeout(timeout.current);
}, []);
if (term === filters.search) {
return;
}

applySearch(term);
}, [debouncedSearch, filters.search]);

if (!currentOrganization) {
return null;
Expand All @@ -92,25 +114,7 @@ export default function ShopsIndex({
const canManageShops =
permissions.canUpdateShop || permissions.canDeleteShop;

const applySearch = (value: string) => {
router.get(
shopsIndex(currentOrganization.slug).url,
value ? { search: value } : {},
{ preserveState: true, preserveScroll: true, replace: true },
);
};

const handleSearchChange = (value: string) => {
setSearch(value);
clearTimeout(searchTimeout.current);
searchTimeout.current = setTimeout(
() => applySearch(value.trim()),
300,
);
};

const clearFilters = () => {
clearTimeout(searchTimeout.current);
setSearch('');
applySearch('');
};
Expand Down Expand Up @@ -201,7 +205,7 @@ export default function ShopsIndex({
type="search"
value={search}
onChange={(event) =>
handleSearchChange(event.target.value)
setSearch(event.target.value)
}
placeholder="Search by name or URL"
aria-label="Search shops"
Expand Down Expand Up @@ -485,6 +489,7 @@ export default function ShopsIndex({
href={shops.prev_page_url}
label="Previous page"
icon={ChevronLeft}
test="pagination-previous"
/>
{shops.links
.slice(1, -1)
Expand Down Expand Up @@ -527,6 +532,7 @@ export default function ShopsIndex({
href={shops.next_page_url}
label="Next page"
icon={ChevronRight}
test="pagination-next"
/>
</nav>
) : null}
Expand All @@ -553,34 +559,6 @@ export default function ShopsIndex({
);
}

function PaginationArrow({
href,
label,
icon: Icon,
}: {
href: string | null;
label: string;
icon: LucideIcon;
}) {
if (!href) {
return (
<Button variant="ghost" size="icon" className="size-8" disabled>
<Icon />
<span className="sr-only">{label}</span>
</Button>
);
}

return (
<Button variant="ghost" size="icon" className="size-8" asChild>
<Link href={href} preserveScroll preserveState>
<Icon />
<span className="sr-only">{label}</span>
</Link>
</Button>
);
}

ShopsIndex.layout = (props: {
currentOrganization?: { slug: string } | null;
}) => ({
Expand Down
Loading