Rework tracker and resource row actions
Turn the category column into an inline Select so a tracker pattern or resource can be recategorised in place. On the pattern row the move is gated by a confirm that surfaces the existing behaviour of promoting or linking a third party. The remaining row actions (include/exclude and delete) move into a single action dropdown, leaving only Edit inline. Add a Max Age column to the pattern table and a Category column to the resource table, and lay the edit forms out with the name above the inputs so the description can use the full row width. Shrink the action column to its content so the freed space goes to the data columns. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -187,8 +187,9 @@ export default function CookieBannerResourcesPage({
|
||||
<Th>{__("Type")}</Th>
|
||||
<SortableTh field="ORIGIN">{__("Origin")}</SortableTh>
|
||||
<Th>{__("Path")}</Th>
|
||||
<Th>{__("Category")}</Th>
|
||||
<SortableTh field="LAST_DETECTED_AT">{__("Last Detected")}</SortableTh>
|
||||
<Th className="w-28" />
|
||||
<Th className="w-px" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
|
||||
@@ -16,9 +16,9 @@ import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Badge,
|
||||
Dropdown,
|
||||
IconArrowBoxLeft,
|
||||
DropdownItem,
|
||||
IconPencil,
|
||||
IconTrashCan,
|
||||
Td,
|
||||
@@ -26,21 +26,16 @@ import {
|
||||
useConfirm,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { Suspense, useCallback, useState } from "react";
|
||||
import { graphql, useFragment, useMutation, useQueryLoader } from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { graphql, useFragment, useMutation } from "react-relay";
|
||||
import { ConnectionHandler } from "relay-runtime";
|
||||
|
||||
import type { MoveToCategoryDropdownQuery } from "#/__generated__/core/MoveToCategoryDropdownQuery.graphql";
|
||||
import type { TrackerResourceRowDeleteMutation } from "#/__generated__/core/TrackerResourceRowDeleteMutation.graphql";
|
||||
import type { TrackerResourceRowFragment$key } from "#/__generated__/core/TrackerResourceRowFragment.graphql";
|
||||
import type { TrackerResourceRowMoveMutation } from "#/__generated__/core/TrackerResourceRowMoveMutation.graphql";
|
||||
import type { TrackerResourceRowUpdateMutation } from "#/__generated__/core/TrackerResourceRowUpdateMutation.graphql";
|
||||
|
||||
import {
|
||||
MoveToCategoryDropdown,
|
||||
moveToCategoryDropdownQuery,
|
||||
} from "../../trackers/_components/MoveToCategoryDropdown";
|
||||
import { MoveToCategorySelect } from "../../trackers/_components/MoveToCategorySelect";
|
||||
|
||||
import { TrackerResourceRowEdit } from "./TrackerResourceRowEdit";
|
||||
|
||||
@@ -54,6 +49,10 @@ const trackerResourceFragment = graphql`
|
||||
description
|
||||
excluded
|
||||
lastDetectedAt
|
||||
cookieCategory {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -147,22 +146,10 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const confirm = useConfirm();
|
||||
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||
const resource = useFragment(trackerResourceFragment, resourceKey);
|
||||
const typeBadge = resourceTypeBadge(resource.type, __);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [categoryQueryRef, loadCategoryQuery]
|
||||
= useQueryLoader<MoveToCategoryDropdownQuery>(moveToCategoryDropdownQuery);
|
||||
|
||||
const handleCategoryDropdownOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open && cookieBannerId) {
|
||||
loadCategoryQuery({ cookieBannerId });
|
||||
}
|
||||
},
|
||||
[loadCategoryQuery, cookieBannerId],
|
||||
);
|
||||
|
||||
const [deleteResource]
|
||||
= useMutation<TrackerResourceRowDeleteMutation>(deleteResourceMutation);
|
||||
@@ -309,6 +296,13 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc
|
||||
<Td>
|
||||
<span className="font-mono text-xs break-all max-w-xs inline-block">{resource.path}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
<MoveToCategorySelect
|
||||
currentCategoryId={resource.cookieCategory?.id}
|
||||
currentCategoryName={resource.cookieCategory?.name}
|
||||
onSelect={handleMove}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{resource.lastDetectedAt
|
||||
? (
|
||||
@@ -318,7 +312,7 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc
|
||||
)
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td>
|
||||
<Td className="w-px whitespace-nowrap">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
@@ -328,40 +322,21 @@ export function TrackerResourceRow({ resourceKey, connectionId }: TrackerResourc
|
||||
>
|
||||
<IconPencil size={14} />
|
||||
</button>
|
||||
<Dropdown
|
||||
onOpenChange={handleCategoryDropdownOpen}
|
||||
toggle={(
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded cursor-pointer"
|
||||
title={__("Move to category")}
|
||||
>
|
||||
<IconArrowBoxLeft size={14} />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
{categoryQueryRef && (
|
||||
<Suspense>
|
||||
<MoveToCategoryDropdown queryRef={categoryQueryRef} onMove={handleMove} />
|
||||
</Suspense>
|
||||
)}
|
||||
</Dropdown>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleExcluded}
|
||||
className="p-1 rounded cursor-pointer"
|
||||
title={resource.excluded ? __("Include") : __("Exclude")}
|
||||
>
|
||||
{resource.excluded ? <EyeIcon size={14} /> : <EyeSlashIcon size={14} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="p-1 rounded cursor-pointer text-danger-dark"
|
||||
title={__("Delete")}
|
||||
>
|
||||
<IconTrashCan size={14} />
|
||||
</button>
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
icon={resource.excluded ? EyeIcon : EyeSlashIcon}
|
||||
onSelect={handleToggleExcluded}
|
||||
>
|
||||
{resource.excluded ? __("Include") : __("Exclude")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onSelect={handleDelete}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -61,7 +61,7 @@ export function TrackerResourceRowEdit({
|
||||
placeholder={__("Display name")}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="pr-3" colSpan={3}>
|
||||
<Td className="pr-3" colSpan={4}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
{...register("description")}
|
||||
|
||||
@@ -272,8 +272,9 @@ export default function CookieBannerTrackersPage({
|
||||
<Th>{__("Third party")}</Th>
|
||||
<SortableTh field="SOURCE">{__("Source")}</SortableTh>
|
||||
<Th>{__("Category")}</Th>
|
||||
<Th>{__("Max Age")}</Th>
|
||||
<SortableTh field="LAST_MATCHED_AT">{__("Last Matched")}</SortableTh>
|
||||
<Th className="w-28" />
|
||||
<Th className="w-px" />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import { Option, Select } from "@probo/ui";
|
||||
import { Suspense, useCallback } from "react";
|
||||
import { type PreloadedQuery, usePreloadedQuery, useQueryLoader } from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
|
||||
import type { MoveToCategoryDropdownQuery } from "#/__generated__/core/MoveToCategoryDropdownQuery.graphql";
|
||||
|
||||
import { moveToCategoryDropdownQuery } from "./MoveToCategoryDropdown";
|
||||
|
||||
interface MoveToCategorySelectProps {
|
||||
currentCategoryId?: string;
|
||||
currentCategoryName?: string;
|
||||
onSelect: (categoryId: string) => void;
|
||||
}
|
||||
|
||||
export function MoveToCategorySelect({
|
||||
currentCategoryId,
|
||||
currentCategoryName,
|
||||
onSelect,
|
||||
}: MoveToCategorySelectProps) {
|
||||
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||
const [categoryQueryRef, loadCategoryQuery]
|
||||
= useQueryLoader<MoveToCategoryDropdownQuery>(moveToCategoryDropdownQuery);
|
||||
|
||||
const handleOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open && cookieBannerId) {
|
||||
loadCategoryQuery({ cookieBannerId });
|
||||
}
|
||||
},
|
||||
[loadCategoryQuery, cookieBannerId],
|
||||
);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
(categoryId: string) => {
|
||||
if (categoryId !== currentCategoryId) {
|
||||
onSelect(categoryId);
|
||||
}
|
||||
},
|
||||
[currentCategoryId, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
variant="ghost"
|
||||
placeholder={currentCategoryName ?? <span className="text-txt-tertiary">-</span>}
|
||||
onValueChange={handleValueChange}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
{categoryQueryRef && (
|
||||
<Suspense>
|
||||
<MoveToCategoryOptions queryRef={categoryQueryRef} />
|
||||
</Suspense>
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
interface MoveToCategoryOptionsProps {
|
||||
queryRef: PreloadedQuery<MoveToCategoryDropdownQuery>;
|
||||
}
|
||||
|
||||
function MoveToCategoryOptions({ queryRef }: MoveToCategoryOptionsProps) {
|
||||
const { __ } = useTranslate();
|
||||
const data = usePreloadedQuery(moveToCategoryDropdownQuery, queryRef);
|
||||
|
||||
if (data.node.__typename !== "CookieBanner") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const categories = data.node.categories.edges.map(e => e.node);
|
||||
|
||||
if (categories.length === 0) {
|
||||
return (
|
||||
<Option value="" disabled className="text-txt-tertiary">
|
||||
{__("No categories")}
|
||||
</Option>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{categories.map(cat => (
|
||||
<Option key={cat.id} value={cat.id}>
|
||||
{cat.name}
|
||||
</Option>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -13,12 +13,12 @@
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||
import { formatError, getTrackerSourceBadge, getTrackerTypeBadge, type GraphQLError } from "@probo/helpers";
|
||||
import { formatError, getTrackerSourceBadge, getTrackerTypeBadge, type GraphQLError, humanizeSeconds } from "@probo/helpers";
|
||||
import { useTranslate } from "@probo/i18n";
|
||||
import {
|
||||
ActionDropdown,
|
||||
Badge,
|
||||
Dropdown,
|
||||
IconArrowBoxLeft,
|
||||
DropdownItem,
|
||||
IconPencil,
|
||||
IconTrashCan,
|
||||
Td,
|
||||
@@ -26,21 +26,16 @@ import {
|
||||
useConfirm,
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { Suspense, useCallback, useState } from "react";
|
||||
import { graphql, useFragment, useMutation, useQueryLoader } from "react-relay";
|
||||
import { useParams } from "react-router";
|
||||
import { useState } from "react";
|
||||
import { graphql, useFragment, useMutation } from "react-relay";
|
||||
import { ConnectionHandler } from "relay-runtime";
|
||||
|
||||
import type { MoveToCategoryDropdownQuery } from "#/__generated__/core/MoveToCategoryDropdownQuery.graphql";
|
||||
import type { TrackerPatternRowDeleteMutation } from "#/__generated__/core/TrackerPatternRowDeleteMutation.graphql";
|
||||
import type { TrackerPatternRowFragment$key } from "#/__generated__/core/TrackerPatternRowFragment.graphql";
|
||||
import type { TrackerPatternRowMoveMutation } from "#/__generated__/core/TrackerPatternRowMoveMutation.graphql";
|
||||
import type { TrackerPatternRowUpdateMutation } from "#/__generated__/core/TrackerPatternRowUpdateMutation.graphql";
|
||||
|
||||
import {
|
||||
MoveToCategoryDropdown,
|
||||
moveToCategoryDropdownQuery,
|
||||
} from "./MoveToCategoryDropdown";
|
||||
import { MoveToCategorySelect } from "./MoveToCategorySelect";
|
||||
import { TrackerPatternRowEdit } from "./TrackerPatternRowEdit";
|
||||
|
||||
const trackerPatternFragment = graphql`
|
||||
@@ -54,6 +49,7 @@ const trackerPatternFragment = graphql`
|
||||
excluded
|
||||
lastMatchedAt
|
||||
cookieCategory {
|
||||
id
|
||||
name
|
||||
}
|
||||
thirdParty {
|
||||
@@ -143,21 +139,9 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const confirm = useConfirm();
|
||||
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||
const pattern = useFragment(trackerPatternFragment, patternKey);
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [categoryQueryRef, loadCategoryQuery]
|
||||
= useQueryLoader<MoveToCategoryDropdownQuery>(moveToCategoryDropdownQuery);
|
||||
|
||||
const handleCategoryDropdownOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open && cookieBannerId) {
|
||||
loadCategoryQuery({ cookieBannerId });
|
||||
}
|
||||
},
|
||||
[loadCategoryQuery, cookieBannerId],
|
||||
);
|
||||
|
||||
const [deletePattern]
|
||||
= useMutation<TrackerPatternRowDeleteMutation>(deletePatternMutation);
|
||||
@@ -229,6 +213,22 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
});
|
||||
};
|
||||
|
||||
const handleMoveWithConfirm = (targetCategoryId: string) => {
|
||||
if (targetCategoryId === pattern.cookieCategory?.id) {
|
||||
return;
|
||||
}
|
||||
confirm(
|
||||
() => {
|
||||
handleMove(targetCategoryId);
|
||||
},
|
||||
{
|
||||
message: __("Moving this tracker to a category will create a third party for it (or link an existing one) if it doesn't have one yet. Continue?"),
|
||||
variant: "primary",
|
||||
label: __("Move"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleToggleExcluded = () => {
|
||||
updatePattern({
|
||||
variables: {
|
||||
@@ -323,10 +323,15 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
? <Badge variant={srcBadge.variant}>{srcBadge.label}</Badge>
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td noLink>
|
||||
<MoveToCategorySelect
|
||||
currentCategoryId={pattern.cookieCategory?.id}
|
||||
currentCategoryName={pattern.cookieCategory?.name}
|
||||
onSelect={handleMoveWithConfirm}
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
{pattern.cookieCategory
|
||||
? <span>{pattern.cookieCategory.name}</span>
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
<span>{humanizeSeconds(pattern.maxAgeSeconds ?? null)}</span>
|
||||
</Td>
|
||||
<Td>
|
||||
{pattern.lastMatchedAt
|
||||
@@ -337,7 +342,7 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
)
|
||||
: <span className="text-txt-tertiary">-</span>}
|
||||
</Td>
|
||||
<Td>
|
||||
<Td noLink className="w-px whitespace-nowrap">
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
@@ -347,40 +352,21 @@ export function TrackerPatternRow({ patternKey, connectionId }: TrackerPatternRo
|
||||
>
|
||||
<IconPencil size={14} />
|
||||
</button>
|
||||
<Dropdown
|
||||
onOpenChange={handleCategoryDropdownOpen}
|
||||
toggle={(
|
||||
<button
|
||||
type="button"
|
||||
className="p-1 rounded cursor-pointer"
|
||||
title={__("Move to category")}
|
||||
>
|
||||
<IconArrowBoxLeft size={14} />
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
{categoryQueryRef && (
|
||||
<Suspense>
|
||||
<MoveToCategoryDropdown queryRef={categoryQueryRef} onMove={handleMove} />
|
||||
</Suspense>
|
||||
)}
|
||||
</Dropdown>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleExcluded}
|
||||
className="p-1 rounded cursor-pointer"
|
||||
title={pattern.excluded ? __("Include") : __("Exclude")}
|
||||
>
|
||||
{pattern.excluded ? <EyeIcon size={14} /> : <EyeSlashIcon size={14} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDelete}
|
||||
className="p-1 rounded cursor-pointer text-danger-dark"
|
||||
title={__("Delete")}
|
||||
>
|
||||
<IconTrashCan size={14} />
|
||||
</button>
|
||||
<ActionDropdown>
|
||||
<DropdownItem
|
||||
icon={pattern.excluded ? EyeIcon : EyeSlashIcon}
|
||||
onSelect={handleToggleExcluded}
|
||||
>
|
||||
{pattern.excluded ? __("Include") : __("Exclude")}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
variant="danger"
|
||||
icon={IconTrashCan}
|
||||
onSelect={handleDelete}
|
||||
>
|
||||
{__("Delete")}
|
||||
</DropdownItem>
|
||||
</ActionDropdown>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
@@ -58,44 +58,45 @@ export function TrackerPatternRowEdit({
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td className="pr-3">
|
||||
<span className="font-medium">{pattern}</span>
|
||||
</Td>
|
||||
<Td />
|
||||
<Td />
|
||||
<Td className="pr-3">
|
||||
<Controller
|
||||
name="duration"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<DurationInput
|
||||
value={field.value.value}
|
||||
unit={field.value.unit}
|
||||
onValueChange={v => field.onChange({ ...field.value, value: v })}
|
||||
onUnitChange={u => field.onChange({ ...field.value, unit: u })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Td>
|
||||
<Td className="pr-3" colSpan={2}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
{...register("description")}
|
||||
placeholder={__("Description")}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={() => void handleSubmit(onSubmit)()}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{__("Save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
<Td colSpan={8}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<span className="font-medium wrap-break-word">{pattern}</span>
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<label className="text-xs text-txt-tertiary">{__("Description")}</label>
|
||||
<Input
|
||||
{...register("description")}
|
||||
placeholder={__("Description")}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-xs text-txt-tertiary">{__("Max Age")}</label>
|
||||
<Controller
|
||||
name="duration"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<DurationInput
|
||||
value={field.value.value}
|
||||
unit={field.value.unit}
|
||||
onValueChange={v => field.onChange({ ...field.value, value: v })}
|
||||
onUnitChange={u => field.onChange({ ...field.value, unit: u })}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => void handleSubmit(onSubmit)()}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{__("Save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
>
|
||||
{__("Cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
|
||||
Reference in New Issue
Block a user