fix: lock map actions while saving
This commit is contained in:
parent
c236efdd72
commit
fd22cc1fe8
@ -29,6 +29,7 @@ export default function LocationMapBottomSheet({
|
|||||||
history,
|
history,
|
||||||
future,
|
future,
|
||||||
saving,
|
saving,
|
||||||
|
savingAction,
|
||||||
hasUnsavedChanges,
|
hasUnsavedChanges,
|
||||||
mapState,
|
mapState,
|
||||||
onAddObject,
|
onAddObject,
|
||||||
@ -74,6 +75,8 @@ export default function LocationMapBottomSheet({
|
|||||||
const changedLayerCount = DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length;
|
const changedLayerCount = DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length;
|
||||||
const unmappedItemPreview = visibleUnmappedItems.slice(0, 8);
|
const unmappedItemPreview = visibleUnmappedItems.slice(0, 8);
|
||||||
const hiddenUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
|
const hiddenUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
|
||||||
|
const saveDraftLabel = savingAction === "save" ? "Saving..." : "Save Draft";
|
||||||
|
const publishLabel = savingAction === "publish" ? "Publishing..." : "Publish";
|
||||||
const showSelectedZoneItems = () => {
|
const showSelectedZoneItems = () => {
|
||||||
setFilters((current) => ({
|
setFilters((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@ -85,21 +88,23 @@ export default function LocationMapBottomSheet({
|
|||||||
const editorActions = showEditorControls ? (
|
const editorActions = showEditorControls ? (
|
||||||
<div className="location-map-editor-action-stack" role="group" aria-label="Map draft actions">
|
<div className="location-map-editor-action-stack" role="group" aria-label="Map draft actions">
|
||||||
<div className="location-map-editor-actions location-map-primary-actions">
|
<div className="location-map-editor-actions location-map-primary-actions">
|
||||||
<button type="button" className="primary" onClick={onSaveDraft} disabled={saving || !hasUnsavedChanges}>Save Draft</button>
|
<button type="button" className="primary" onClick={onSaveDraft} disabled={saving || !hasUnsavedChanges}>
|
||||||
|
{saveDraftLabel}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="primary"
|
className="primary"
|
||||||
onClick={onPublish}
|
onClick={onPublish}
|
||||||
disabled={saving || (!mapState?.draft_map && !hasUnsavedChanges)}
|
disabled={saving || (!mapState?.draft_map && !hasUnsavedChanges)}
|
||||||
>
|
>
|
||||||
Publish
|
{publishLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="location-map-editor-actions location-map-secondary-actions">
|
<div className="location-map-editor-actions location-map-secondary-actions">
|
||||||
<button type="button" onClick={onAddObject}>Add Area</button>
|
<button type="button" onClick={onAddObject} disabled={saving}>Add Area</button>
|
||||||
<button type="button" onClick={onPreviewDraft} aria-label="Preview Draft">Preview</button>
|
<button type="button" onClick={onPreviewDraft} aria-label="Preview Draft" disabled={saving}>Preview</button>
|
||||||
<button type="button" onClick={onUndo} disabled={history.length === 0}>Undo</button>
|
<button type="button" onClick={onUndo} disabled={saving || history.length === 0}>Undo</button>
|
||||||
<button type="button" onClick={onRedo} disabled={future.length === 0}>Redo</button>
|
<button type="button" onClick={onRedo} disabled={saving || future.length === 0}>Redo</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : canManage && mapState?.published_map ? (
|
) : canManage && mapState?.published_map ? (
|
||||||
@ -172,6 +177,7 @@ export default function LocationMapBottomSheet({
|
|||||||
type="button"
|
type="button"
|
||||||
className={[editorTool === "pan" ? "active" : "", "is-pan"].filter(Boolean).join(" ")}
|
className={[editorTool === "pan" ? "active" : "", "is-pan"].filter(Boolean).join(" ")}
|
||||||
onClick={onPanMode}
|
onClick={onPanMode}
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
Pan
|
Pan
|
||||||
</button>
|
</button>
|
||||||
@ -179,6 +185,7 @@ export default function LocationMapBottomSheet({
|
|||||||
type="button"
|
type="button"
|
||||||
className={[editorTool === "edit" ? "active" : "", "is-edit"].filter(Boolean).join(" ")}
|
className={[editorTool === "edit" ? "active" : "", "is-edit"].filter(Boolean).join(" ")}
|
||||||
onClick={onEditObjects}
|
onClick={onEditObjects}
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
Objects
|
Objects
|
||||||
</button>
|
</button>
|
||||||
@ -191,6 +198,7 @@ export default function LocationMapBottomSheet({
|
|||||||
Label
|
Label
|
||||||
<input
|
<input
|
||||||
value={selectedObject.label || ""}
|
value={selectedObject.label || ""}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onObjectField("label", event.target.value)}
|
onChange={(event) => onObjectField("label", event.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -199,6 +207,7 @@ export default function LocationMapBottomSheet({
|
|||||||
<select
|
<select
|
||||||
aria-label="Object type"
|
aria-label="Object type"
|
||||||
value={selectedObject.type}
|
value={selectedObject.type}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onObjectField("type", event.target.value)}
|
onChange={(event) => onObjectField("type", event.target.value)}
|
||||||
>
|
>
|
||||||
{MAP_OBJECT_TYPES.map((type) => (
|
{MAP_OBJECT_TYPES.map((type) => (
|
||||||
@ -211,6 +220,7 @@ export default function LocationMapBottomSheet({
|
|||||||
<select
|
<select
|
||||||
aria-label="Linked zone"
|
aria-label="Linked zone"
|
||||||
value={objectZoneId(selectedObject)}
|
value={objectZoneId(selectedObject)}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onZoneLinkChange(event.target.value)}
|
onChange={(event) => onZoneLinkChange(event.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">No linked zone</option>
|
<option value="">No linked zone</option>
|
||||||
@ -225,6 +235,7 @@ export default function LocationMapBottomSheet({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={Math.round(selectedObject.x)}
|
value={Math.round(selectedObject.x)}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onObjectField("x", Number(event.target.value))}
|
onChange={(event) => onObjectField("x", Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -233,6 +244,7 @@ export default function LocationMapBottomSheet({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={Math.round(selectedObject.y)}
|
value={Math.round(selectedObject.y)}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onObjectField("y", Number(event.target.value))}
|
onChange={(event) => onObjectField("y", Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -241,6 +253,7 @@ export default function LocationMapBottomSheet({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={Math.round(selectedObject.width)}
|
value={Math.round(selectedObject.width)}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onObjectField("width", Number(event.target.value))}
|
onChange={(event) => onObjectField("width", Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@ -249,14 +262,15 @@ export default function LocationMapBottomSheet({
|
|||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={Math.round(selectedObject.height)}
|
value={Math.round(selectedObject.height)}
|
||||||
|
disabled={saving}
|
||||||
onChange={(event) => onObjectField("height", Number(event.target.value))}
|
onChange={(event) => onObjectField("height", Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="location-map-editor-actions">
|
<div className="location-map-editor-actions">
|
||||||
<button type="button" onClick={onClearSelection}>Done</button>
|
<button type="button" onClick={onClearSelection}>Done</button>
|
||||||
<button type="button" onClick={onDuplicateObject}>Duplicate</button>
|
<button type="button" onClick={onDuplicateObject} disabled={saving}>Duplicate</button>
|
||||||
<button type="button" className="danger" onClick={onDeleteObject}>Delete</button>
|
<button type="button" className="danger" onClick={onDeleteObject} disabled={saving}>Delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : mode === "edit" && editorTool === "edit" ? (
|
) : mode === "edit" && editorTool === "edit" ? (
|
||||||
|
|||||||
@ -19,12 +19,13 @@ export default function LocationMapCanvas({
|
|||||||
username,
|
username,
|
||||||
svgRef,
|
svgRef,
|
||||||
dragState,
|
dragState,
|
||||||
|
editingLocked = false,
|
||||||
setDragState,
|
setDragState,
|
||||||
setSelectedObjectKey,
|
setSelectedObjectKey,
|
||||||
remember,
|
remember,
|
||||||
updateObjects,
|
updateObjects,
|
||||||
}) {
|
}) {
|
||||||
const canEditObjects = mode === "edit" && editorTool === "edit";
|
const canEditObjects = !editingLocked && mode === "edit" && editorTool === "edit";
|
||||||
const canDragPan = mode === "view" || editorTool === "pan";
|
const canDragPan = mode === "view" || editorTool === "pan";
|
||||||
const scrollRef = useRef(null);
|
const scrollRef = useRef(null);
|
||||||
const panDragRef = useRef(null);
|
const panDragRef = useRef(null);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export default function LocationMapSetupPanel({
|
|||||||
canManage,
|
canManage,
|
||||||
zoneCount = 0,
|
zoneCount = 0,
|
||||||
saving,
|
saving,
|
||||||
|
savingAction,
|
||||||
onContinue,
|
onContinue,
|
||||||
onPreview,
|
onPreview,
|
||||||
onPublish,
|
onPublish,
|
||||||
@ -20,6 +21,9 @@ export default function LocationMapSetupPanel({
|
|||||||
const createFromZonesLabel = hasZones
|
const createFromZonesLabel = hasZones
|
||||||
? `Create From ${zoneCount} Zone${zoneCount === 1 ? "" : "s"}`
|
? `Create From ${zoneCount} Zone${zoneCount === 1 ? "" : "s"}`
|
||||||
: "Create From Zones";
|
: "Create From Zones";
|
||||||
|
const createBlankLabel = savingAction === "create-blank" ? "Creating..." : "Create Blank Map";
|
||||||
|
const createZonesButtonLabel = savingAction === "create-zones" ? "Creating..." : createFromZonesLabel;
|
||||||
|
const publishLabel = savingAction === "publish" ? "Publishing..." : "Publish Map";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="location-map-setup">
|
<section className="location-map-setup">
|
||||||
@ -27,14 +31,14 @@ export default function LocationMapSetupPanel({
|
|||||||
<p>{description}</p>
|
<p>{description}</p>
|
||||||
{hasAnyMap && canManage ? (
|
{hasAnyMap && canManage ? (
|
||||||
<div className="location-map-setup-actions">
|
<div className="location-map-setup-actions">
|
||||||
<button type="button" className="btn-primary" onClick={onContinue}>
|
<button type="button" className="btn-primary" onClick={onContinue} disabled={saving}>
|
||||||
Continue Editing
|
Continue Editing
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn-secondary" onClick={onPreview}>
|
<button type="button" className="btn-secondary" onClick={onPreview} disabled={saving}>
|
||||||
Preview Map
|
Preview Map
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn-primary" onClick={onPublish} disabled={saving || !canManage}>
|
<button type="button" className="btn-primary" onClick={onPublish} disabled={saving || !canManage}>
|
||||||
Publish Map
|
{publishLabel}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : !hasAnyMap && canManage ? (
|
) : !hasAnyMap && canManage ? (
|
||||||
@ -47,7 +51,7 @@ export default function LocationMapSetupPanel({
|
|||||||
{!hasZones ? (
|
{!hasZones ? (
|
||||||
<>
|
<>
|
||||||
<button type="button" className="btn-primary" onClick={onCreateBlank} disabled={saving}>
|
<button type="button" className="btn-primary" onClick={onCreateBlank} disabled={saving}>
|
||||||
Create Blank Map
|
{createBlankLabel}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn-secondary" onClick={onCreateFromZones} disabled>
|
<button type="button" className="btn-secondary" onClick={onCreateFromZones} disabled>
|
||||||
{createFromZonesLabel}
|
{createFromZonesLabel}
|
||||||
@ -56,10 +60,10 @@ export default function LocationMapSetupPanel({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button type="button" className="btn-primary" onClick={onCreateFromZones} disabled={saving}>
|
<button type="button" className="btn-primary" onClick={onCreateFromZones} disabled={saving}>
|
||||||
{createFromZonesLabel}
|
{createZonesButtonLabel}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn-secondary" onClick={onCreateBlank} disabled={saving}>
|
<button type="button" className="btn-secondary" onClick={onCreateBlank} disabled={saving}>
|
||||||
Create Blank Map
|
{createBlankLabel}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export default function LocationMapToolbar({
|
|||||||
editorTool,
|
editorTool,
|
||||||
hasAnyMap,
|
hasAnyMap,
|
||||||
canManage,
|
canManage,
|
||||||
|
saving,
|
||||||
zoom,
|
zoom,
|
||||||
setZoom,
|
setZoom,
|
||||||
onFit,
|
onFit,
|
||||||
@ -18,7 +19,7 @@ export default function LocationMapToolbar({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={mode === "view" ? "active" : ""}
|
className={mode === "view" ? "active" : ""}
|
||||||
disabled={!hasAnyMap}
|
disabled={saving || !hasAnyMap}
|
||||||
onClick={onView}
|
onClick={onView}
|
||||||
aria-pressed={mode === "view"}
|
aria-pressed={mode === "view"}
|
||||||
>
|
>
|
||||||
@ -28,7 +29,7 @@ export default function LocationMapToolbar({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={mode === "edit" ? "active" : ""}
|
className={mode === "edit" ? "active" : ""}
|
||||||
disabled={!hasAnyMap}
|
disabled={saving || !hasAnyMap}
|
||||||
onClick={onEdit}
|
onClick={onEdit}
|
||||||
aria-pressed={mode === "edit"}
|
aria-pressed={mode === "edit"}
|
||||||
aria-label="Edit Draft"
|
aria-label="Edit Draft"
|
||||||
@ -44,6 +45,7 @@ export default function LocationMapToolbar({
|
|||||||
type="button"
|
type="button"
|
||||||
className={editorTool === "pan" ? "active is-pan" : ""}
|
className={editorTool === "pan" ? "active is-pan" : ""}
|
||||||
onClick={onPanMode}
|
onClick={onPanMode}
|
||||||
|
disabled={saving}
|
||||||
aria-pressed={editorTool === "pan"}
|
aria-pressed={editorTool === "pan"}
|
||||||
>
|
>
|
||||||
Pan Mode
|
Pan Mode
|
||||||
@ -52,6 +54,7 @@ export default function LocationMapToolbar({
|
|||||||
type="button"
|
type="button"
|
||||||
className={editorTool === "edit" ? "active is-edit" : ""}
|
className={editorTool === "edit" ? "active is-edit" : ""}
|
||||||
onClick={onEditObjects}
|
onClick={onEditObjects}
|
||||||
|
disabled={saving}
|
||||||
aria-pressed={editorTool === "edit"}
|
aria-pressed={editorTool === "edit"}
|
||||||
>
|
>
|
||||||
Edit Objects
|
Edit Objects
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export default function LocationMapManager() {
|
|||||||
const [mapState, setMapState] = useState(null);
|
const [mapState, setMapState] = useState(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [savingAction, setSavingAction] = useState(null);
|
||||||
const [mode, setMode] = useState("setup");
|
const [mode, setMode] = useState("setup");
|
||||||
const [editorTool, setEditorTool] = useState("pan");
|
const [editorTool, setEditorTool] = useState("pan");
|
||||||
const [previewDraft, setPreviewDraft] = useState(false);
|
const [previewDraft, setPreviewDraft] = useState(false);
|
||||||
@ -189,9 +190,19 @@ export default function LocationMapManager() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const beginSaving = (action) => {
|
||||||
|
setSavingAction(action);
|
||||||
|
setSaving(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const endSaving = () => {
|
||||||
|
setSaving(false);
|
||||||
|
setSavingAction(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateBlank = async () => {
|
const handleCreateBlank = async () => {
|
||||||
if (!activeHousehold?.id || !locationId) return;
|
if (!activeHousehold?.id || !locationId) return;
|
||||||
setSaving(true);
|
beginSaving("create-blank");
|
||||||
try {
|
try {
|
||||||
const response = await createBlankLocationMap(activeHousehold.id, locationId, mapDraft);
|
const response = await createBlankLocationMap(activeHousehold.id, locationId, mapDraft);
|
||||||
setMapState(response.data);
|
setMapState(response.data);
|
||||||
@ -203,13 +214,13 @@ export default function LocationMapManager() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map"));
|
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map"));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
endSaving();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateFromZones = async () => {
|
const handleCreateFromZones = async () => {
|
||||||
if (!activeHousehold?.id || !locationId) return;
|
if (!activeHousehold?.id || !locationId) return;
|
||||||
setSaving(true);
|
beginSaving("create-zones");
|
||||||
try {
|
try {
|
||||||
const response = await createLocationMapFromZones(activeHousehold.id, locationId, mapDraft);
|
const response = await createLocationMapFromZones(activeHousehold.id, locationId, mapDraft);
|
||||||
setMapState(response.data);
|
setMapState(response.data);
|
||||||
@ -221,13 +232,13 @@ export default function LocationMapManager() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map from zones"));
|
toast.error("Create map failed", getApiErrorMessage(error, "Failed to create map from zones"));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
endSaving();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveDraft = async () => {
|
const handleSaveDraft = async () => {
|
||||||
if (!activeHousehold?.id || !locationId) return;
|
if (!activeHousehold?.id || !locationId) return;
|
||||||
setSaving(true);
|
beginSaving("save");
|
||||||
try {
|
try {
|
||||||
const response = await saveLocationMapDraft(activeHousehold.id, locationId, {
|
const response = await saveLocationMapDraft(activeHousehold.id, locationId, {
|
||||||
map: mapDraft,
|
map: mapDraft,
|
||||||
@ -241,13 +252,13 @@ export default function LocationMapManager() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft"));
|
toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft"));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
endSaving();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePublish = async () => {
|
const handlePublish = async () => {
|
||||||
if (!activeHousehold?.id || !locationId) return;
|
if (!activeHousehold?.id || !locationId) return;
|
||||||
setSaving(true);
|
beginSaving("publish");
|
||||||
try {
|
try {
|
||||||
if (hasUnsavedChanges) {
|
if (hasUnsavedChanges) {
|
||||||
await saveLocationMapDraft(activeHousehold.id, locationId, {
|
await saveLocationMapDraft(activeHousehold.id, locationId, {
|
||||||
@ -271,7 +282,7 @@ export default function LocationMapManager() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map"));
|
toast.error("Publish failed", getApiErrorMessage(error, "Failed to publish map"));
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
endSaving();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -461,6 +472,7 @@ export default function LocationMapManager() {
|
|||||||
editorTool={editorTool}
|
editorTool={editorTool}
|
||||||
hasAnyMap={hasAnyMap}
|
hasAnyMap={hasAnyMap}
|
||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
|
saving={saving}
|
||||||
zoom={zoom}
|
zoom={zoom}
|
||||||
setZoom={setZoom}
|
setZoom={setZoom}
|
||||||
onFit={handleFitMap}
|
onFit={handleFitMap}
|
||||||
@ -477,6 +489,7 @@ export default function LocationMapManager() {
|
|||||||
canManage={canManage}
|
canManage={canManage}
|
||||||
zoneCount={mapState?.zones?.length || 0}
|
zoneCount={mapState?.zones?.length || 0}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
|
savingAction={savingAction}
|
||||||
onContinue={() => setMode("edit")}
|
onContinue={() => setMode("edit")}
|
||||||
onPreview={handlePreviewDraft}
|
onPreview={handlePreviewDraft}
|
||||||
onPublish={handlePublish}
|
onPublish={handlePublish}
|
||||||
@ -497,6 +510,7 @@ export default function LocationMapManager() {
|
|||||||
username={username}
|
username={username}
|
||||||
svgRef={svgRef}
|
svgRef={svgRef}
|
||||||
dragState={dragState}
|
dragState={dragState}
|
||||||
|
editingLocked={saving}
|
||||||
setDragState={setDragState}
|
setDragState={setDragState}
|
||||||
setSelectedObjectKey={setSelectedObjectKey}
|
setSelectedObjectKey={setSelectedObjectKey}
|
||||||
remember={remember}
|
remember={remember}
|
||||||
@ -516,6 +530,7 @@ export default function LocationMapManager() {
|
|||||||
history={history}
|
history={history}
|
||||||
future={future}
|
future={future}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
|
savingAction={savingAction}
|
||||||
hasUnsavedChanges={hasUnsavedChanges}
|
hasUnsavedChanges={hasUnsavedChanges}
|
||||||
mapState={mapState}
|
mapState={mapState}
|
||||||
onAddObject={handleAddObject}
|
onAddObject={handleAddObject}
|
||||||
|
|||||||
@ -175,6 +175,7 @@
|
|||||||
.location-map-tool-buttons button:disabled,
|
.location-map-tool-buttons button:disabled,
|
||||||
.location-map-zoom-controls button:disabled,
|
.location-map-zoom-controls button:disabled,
|
||||||
.location-map-editor-actions button:disabled,
|
.location-map-editor-actions button:disabled,
|
||||||
|
.location-map-mobile-tool-switch button:disabled,
|
||||||
.location-map-setup-actions button:disabled {
|
.location-map-setup-actions button:disabled {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|||||||
@ -596,6 +596,79 @@ test("admin selecting an object does not mark a draft dirty until it changes", a
|
|||||||
await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled();
|
await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("admin save progress locks draft controls", async ({ page }) => {
|
||||||
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
let mapState = draftMapState([
|
||||||
|
{
|
||||||
|
id: 1361,
|
||||||
|
location_map_id: 900,
|
||||||
|
zone_id: 501,
|
||||||
|
zone_name: "Bakery",
|
||||||
|
type: "zone",
|
||||||
|
label: "Bakery",
|
||||||
|
x: 40,
|
||||||
|
y: 40,
|
||||||
|
width: 260,
|
||||||
|
height: 160,
|
||||||
|
rotation: 0,
|
||||||
|
locked: false,
|
||||||
|
visible: true,
|
||||||
|
sort_order: 1,
|
||||||
|
},
|
||||||
|
], true);
|
||||||
|
let releaseSave: () => void = () => {};
|
||||||
|
let saveStarted = false;
|
||||||
|
const saveGate = new Promise<void>((resolve) => {
|
||||||
|
releaseSave = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/households/1/locations/10/map", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mapState),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route("**/households/1/locations/10/map/draft", async (route) => {
|
||||||
|
saveStarted = true;
|
||||||
|
const payload = await route.request().postDataJSON();
|
||||||
|
await saveGate;
|
||||||
|
const savedObjects = (payload.objects as Array<Record<string, unknown>>).map((object, index) => ({
|
||||||
|
...(mapState.draft_objects[index] || {}),
|
||||||
|
...object,
|
||||||
|
}));
|
||||||
|
mapState = draftMapState(savedObjects, true);
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mapState),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/stores/100/locations/10/map");
|
||||||
|
await page.getByRole("button", { name: "Continue Editing" }).click();
|
||||||
|
await page.getByRole("button", { name: "Edit Objects" }).click();
|
||||||
|
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
|
||||||
|
await page.getByRole("textbox", { name: "Label" }).fill("Saving Bakery");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Save Draft" }).click();
|
||||||
|
await expect.poll(() => saveStarted).toBe(true);
|
||||||
|
await expect(page.getByRole("button", { name: "Saving..." })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("button", { name: "Publish" })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("button", { name: "Add Area" })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("button", { name: "Preview Draft" })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("button", { name: "Duplicate" })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("button", { name: "Delete" })).toBeDisabled();
|
||||||
|
await expect(page.getByRole("textbox", { name: "Label" })).toBeDisabled();
|
||||||
|
|
||||||
|
releaseSave();
|
||||||
|
await expect(page.locator(".location-map-status")).toHaveText("Draft");
|
||||||
|
await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
test("admin publish saves pending map edits before publishing", async ({ page }) => {
|
test("admin publish saves pending map edits before publishing", async ({ page }) => {
|
||||||
await mockMapShell(page);
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user