{content}
{action && (
diff --git a/packages/admin-ui/src/Sidebar/components/items/SidebarMenuSubItem.tsx b/packages/admin-ui/src/Sidebar/components/items/SidebarMenuSubItem.tsx
index 51ef38c6e07..e476b357489 100644
--- a/packages/admin-ui/src/Sidebar/components/items/SidebarMenuSubItem.tsx
+++ b/packages/admin-ui/src/Sidebar/components/items/SidebarMenuSubItem.tsx
@@ -161,7 +161,7 @@ const SidebarMenuSubItem = ({
if (!collapsible) {
return (
-
+
-
+
{sidebarMenuSubButton}
diff --git a/packages/admin-ui/src/Tree/useTree.ts b/packages/admin-ui/src/Tree/useTree.ts
index 2de951186da..aa3edcf0246 100644
--- a/packages/admin-ui/src/Tree/useTree.ts
+++ b/packages/admin-ui/src/Tree/useTree.ts
@@ -56,8 +56,6 @@ export const useTree = >(props: TreeProps) =>
});
});
- await presenter.handleDrop(newNodeTree);
-
if (props.onDrop) {
const { dragSourceId, dropTargetId } = options;
@@ -70,8 +68,14 @@ export const useTree = >(props: TreeProps) =>
dropTarget: newTreeDto.find(node => node.id === String(options.dropTargetId))
};
- await props.onDrop(newTreeDto, dropOptions);
+ try {
+ await props.onDrop(newTreeDto, dropOptions);
+ } catch {
+ return;
+ }
}
+
+ await presenter.handleDrop(newNodeTree);
};
const changeOpen = (newOpenIds: NodeModel["id"][]) => {
diff --git a/packages/api-scheduler/__tests__/SchedulerService.test.ts b/packages/api-scheduler/__tests__/SchedulerService.test.ts
index 32070cd07f9..24aee76215f 100644
--- a/packages/api-scheduler/__tests__/SchedulerService.test.ts
+++ b/packages/api-scheduler/__tests__/SchedulerService.test.ts
@@ -5,6 +5,7 @@ import {
CreateScheduleCommand,
DeleteScheduleCommand,
GetScheduleCommand,
+ ResourceNotFoundException,
SchedulerClient,
UpdateScheduleCommand
} from "@webiny/aws-sdk/client-scheduler/index.js";
@@ -46,6 +47,15 @@ describe("SchedulerService", () => {
it("throws if creating a schedule in the past", async () => {
const client = mockClient(SchedulerClient);
+ client.on(GetScheduleCommand).rejects(
+ new ResourceNotFoundException({
+ Message: "Resource not found.",
+ message: "Resource not found.",
+ $metadata: {
+ httpStatusCode: 404
+ }
+ })
+ );
const service = new EventBridgeSchedulerService(() => client, config);
const input: SchedulerServiceCreateInput = {
diff --git a/packages/api-scheduler/src/features/SchedulerService/EventBridgeSchedulerService.ts b/packages/api-scheduler/src/features/SchedulerService/EventBridgeSchedulerService.ts
index 7b73a4ef57b..20307143170 100644
--- a/packages/api-scheduler/src/features/SchedulerService/EventBridgeSchedulerService.ts
+++ b/packages/api-scheduler/src/features/SchedulerService/EventBridgeSchedulerService.ts
@@ -9,7 +9,8 @@ import {
DeleteScheduleCommand,
GetScheduleCommand,
type SchedulerClient,
- UpdateScheduleCommand
+ UpdateScheduleCommand,
+ ResourceNotFoundException
} from "@webiny/aws-sdk/client-scheduler/index.js";
import { SCHEDULED_ACTION_EVENT_IDENTIFIER } from "~/constants.js";
import type { IScheduledActionEventPayload } from "~/createEventHandler.js";
@@ -34,6 +35,66 @@ export class EventBridgeSchedulerService implements SchedulerService.Interface {
) {}
public async create(params: SchedulerService.CreateParams): Promise {
+ // Check if schedule already exists (for auto-update logic)
+ const exists = await this.exists(params);
+ if (exists) {
+ return this.updateInternal(params);
+ }
+
+ return await this.createInternal(params);
+ }
+
+ public async update(params: SchedulerService.UpdateParams): Promise {
+ const exists = await this.exists(params);
+ if (exists) {
+ return await this.updateInternal(params);
+ }
+ return await this.create(params);
+ }
+
+ public async delete(params: SchedulerService.DeleteParams): Promise {
+ const client = this.getClient();
+
+ const exists = await this.exists(params);
+ if (!exists) {
+ throw new WebinyError(
+ `Cannot delete schedule "${params.id}", tenant "${params.tenant}", because it does not exist.`
+ );
+ }
+
+ const name = this.createScheduleName(params);
+
+ try {
+ await client.send(new DeleteScheduleCommand({ Name: name }));
+ } catch (ex) {
+ if (ex instanceof ResourceNotFoundException) {
+ return;
+ } else if (ex.name === "ResourceNotFoundException") {
+ return;
+ }
+ throw WebinyError.from(ex);
+ }
+ }
+
+ public async exists(params: SchedulerService.ExistsParams): Promise {
+ const client = this.getClient();
+
+ const name = this.createScheduleName(params);
+
+ try {
+ await client.send(new GetScheduleCommand({ Name: name }));
+ return true;
+ } catch (ex) {
+ if (ex instanceof ResourceNotFoundException) {
+ return false;
+ } else if (ex.name === "ResourceNotFoundException") {
+ return false;
+ }
+ throw ex;
+ }
+ }
+
+ private async createInternal(params: SchedulerService.CreateParams): Promise {
const { id, scheduleFor, tenant } = params;
// Validate date is in future
@@ -51,12 +112,6 @@ export class EventBridgeSchedulerService implements SchedulerService.Interface {
const client = this.getClient();
- // Check if schedule already exists (for auto-update logic)
- const exists = await this.exists(params);
- if (exists) {
- return this.update(params);
- }
-
await client.send(
new CreateScheduleCommand({
Name: this.createScheduleName(params),
@@ -74,7 +129,7 @@ export class EventBridgeSchedulerService implements SchedulerService.Interface {
);
}
- public async update(params: SchedulerService.UpdateParams): Promise {
+ private async updateInternal(params: SchedulerService.UpdateParams): Promise {
const { id, scheduleFor } = params;
// Validate date is in future
@@ -108,44 +163,6 @@ export class EventBridgeSchedulerService implements SchedulerService.Interface {
);
}
- public async delete(params: SchedulerService.DeleteParams): Promise {
- const client = this.getClient();
-
- const exists = await this.exists(params);
- if (!exists) {
- throw new WebinyError(
- `Cannot delete schedule "${params.id}", tenant "${params.tenant}", because it does not exist.`
- );
- }
-
- const name = this.createScheduleName(params);
-
- try {
- await client.send(new DeleteScheduleCommand({ Name: name }));
- } catch (ex) {
- if (ex.name === "ResourceNotFoundException") {
- return;
- }
- throw WebinyError.from(ex);
- }
- }
-
- public async exists(params: SchedulerService.ExistsParams): Promise {
- const client = this.getClient();
-
- const name = this.createScheduleName(params);
-
- try {
- await client.send(new GetScheduleCommand({ Name: name }));
- return true;
- } catch (ex) {
- if (ex.name === "ResourceNotFoundException") {
- return false;
- }
- throw ex;
- }
- }
-
private createScheduleExpression(scheduleFor: Date): string {
// Format: at(YYYY-MM-DDTHH:mm:ss) - EventBridge expects this format
return `at(${scheduleFor.toISOString().replace(/\.\d{3}Z$/, "")})`;
diff --git a/packages/app-aco/src/components/FolderTree/List/List.tsx b/packages/app-aco/src/components/FolderTree/List/List.tsx
index c95973b7dc5..fe6c2d0a6e5 100644
--- a/packages/app-aco/src/components/FolderTree/List/List.tsx
+++ b/packages/app-aco/src/components/FolderTree/List/List.tsx
@@ -98,17 +98,25 @@ export const List = ({
// Abort if either folder is not found
if (!folder || !targetFolder) {
- return;
+ throw new Error("Folder not found");
}
- showConfirmMoveFolderDialog({
- folder,
- targetFolder,
- onAccept: runDrop
+ await new Promise((resolve, reject) => {
+ showConfirmMoveFolderDialog({
+ folder,
+ targetFolder,
+ onAccept: async () => {
+ await runDrop();
+ resolve();
+ },
+ onClose: () => {
+ reject(new Error("cancelled"));
+ }
+ });
});
} else {
- // Otherwise, perform the drop immediately
- await runDrop();
+ // Fire API call without awaiting — optimistic UI
+ runDrop();
}
},
[folders, dropConfirmation, showConfirmMoveFolderDialog]
diff --git a/packages/app-aco/src/dialogs/useConfirmMoveFolderDialog.tsx b/packages/app-aco/src/dialogs/useConfirmMoveFolderDialog.tsx
index fbfca8be4d5..5a8217aedb0 100644
--- a/packages/app-aco/src/dialogs/useConfirmMoveFolderDialog.tsx
+++ b/packages/app-aco/src/dialogs/useConfirmMoveFolderDialog.tsx
@@ -5,6 +5,7 @@ interface ShowDialogParams {
folder: FolderDto;
targetFolder: FolderDto;
onAccept: (folder: FolderDto, targetFolder: FolderDto) => Promise;
+ onClose?: () => void;
}
interface UseConfirmMoveFolderDialogResponse {
@@ -14,14 +15,15 @@ interface UseConfirmMoveFolderDialogResponse {
export const useConfirmMoveFolderDialog = (): UseConfirmMoveFolderDialogResponse => {
const dialogs = useDialogs();
- const showDialog = ({ folder, targetFolder, onAccept }: ShowDialogParams) => {
+ const showDialog = ({ folder, targetFolder, onAccept, onClose }: ShowDialogParams) => {
dialogs.showDialog({
title: "Move folder",
content: `You are about to move the folder "${folder.title}" into "${targetFolder.title}"! Are you sure you want to continue?`,
acceptLabel: "Move folder",
cancelLabel: "Cancel",
loadingLabel: "Moving folder...",
- onAccept: () => onAccept(folder, targetFolder)
+ onAccept: () => onAccept(folder, targetFolder),
+ onClose
});
};
diff --git a/packages/app-headless-cms/src/admin/components/ContentEntries/SidebarContent/SidebarContent.tsx b/packages/app-headless-cms/src/admin/components/ContentEntries/SidebarContent/SidebarContent.tsx
index b969c9c8eee..f535c070d5c 100644
--- a/packages/app-headless-cms/src/admin/components/ContentEntries/SidebarContent/SidebarContent.tsx
+++ b/packages/app-headless-cms/src/admin/components/ContentEntries/SidebarContent/SidebarContent.tsx
@@ -10,6 +10,7 @@ export const SidebarContent = () => {
navigateToFolder(data.id)}
enableActions={true}
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGridItem.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGridItem.tsx
index 741ce635e25..4a7795856b9 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGridItem.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateGridItem.tsx
@@ -36,7 +36,7 @@ export const TemplateGridItem = makeDecoratable(
{template.name}
-
+
{template.description}
diff --git a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateListItem.tsx b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateListItem.tsx
index 6ef788ce101..6294cc18818 100644
--- a/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateListItem.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fieldRenderers/dynamicZone/TemplateListItem.tsx
@@ -28,7 +28,7 @@ export const TemplateListItem = ({ template, onTemplate }: TemplateListItemProps
{template.name}
{template.description && (
-
+
{template.description}
)}
diff --git a/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZone.tsx b/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZone.tsx
index d566f5c8d7d..5d7e08bb4e2 100644
--- a/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZone.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZone.tsx
@@ -1,8 +1,9 @@
-import React, { useRef, useEffect } from "react";
+import React, { useRef, useEffect, useState, useCallback } from "react";
import { Accordion } from "@webiny/admin-ui";
import type { CmsDynamicZoneTemplate } from "~/types.js";
import { AddTemplateIcon, AddTemplateButton } from "./AddTemplate.js";
import { DynamicZoneTemplate } from "./DynamicZoneTemplate.js";
+import { TemplateDialog } from "./TemplateDialog.js";
import { useModelField, useModelFieldEditor } from "~/admin/hooks/index.js";
function updateOrCreateTemplate(
@@ -26,6 +27,9 @@ export const DynamicZone = () => {
const { field } = useModelField();
const { updateField } = useModelFieldEditor();
const newTemplateId = useRef(undefined);
+ const [templateToEdit, setTemplateToEdit] = useState(
+ undefined
+ );
const templates: CmsDynamicZoneTemplate[] = field.settings?.templates || [];
@@ -43,13 +47,23 @@ export const DynamicZone = () => {
});
};
+ const onDialogClose = useCallback(() => {
+ setTemplateToEdit(undefined);
+ }, []);
+
useEffect(() => {
- // We only want to open the accordion item on first mount of a new template.
newTemplateId.current = undefined;
}, []);
return (
<>
+ {templateToEdit ? (
+
+ ) : null}
{templates.length ? (
{templates.map((template, index) => (
@@ -60,6 +74,7 @@ export const DynamicZone = () => {
field={field}
template={template}
onChange={updateField}
+ onEditTemplate={setTemplateToEdit}
/>
))}
diff --git a/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZoneTemplate.tsx b/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZoneTemplate.tsx
index 3614de6dc37..78308d5d4b9 100644
--- a/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZoneTemplate.tsx
+++ b/packages/app-headless-cms/src/admin/plugins/fields/dynamicZone/DynamicZoneTemplate.tsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from "react";
+import React, { useCallback } from "react";
import { set } from "dot-prop-immutable";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ReactComponent as EditIcon } from "@webiny/icons/edit.svg";
@@ -13,7 +13,6 @@ import {
removeValueAtIndex
} from "~/admin/plugins/arrayUtils.js";
import type { CmsDynamicZoneTemplate, CmsEditorFieldsLayout, CmsModelField } from "~/types.js";
-import { TemplateDialog } from "./TemplateDialog.js";
import { FieldEditor } from "~/admin/components/FieldEditor/index.js";
import { normalizeIcon } from "~/utils/normalizeIcon.js";
@@ -23,6 +22,7 @@ interface DynamicZoneTemplateProps {
template: CmsDynamicZoneTemplate;
onChange: (field: CmsModelField) => void;
open: boolean;
+ onEditTemplate: (template: CmsDynamicZoneTemplate) => void;
}
interface UpdateTemplate {
@@ -40,7 +40,8 @@ export const DynamicZoneTemplate = ({
field,
template,
onChange,
- open
+ open,
+ onEditTemplate
}: DynamicZoneTemplateProps) => {
const { showConfirmation } = useConfirmationDialog({
title: "Delete content template",
@@ -48,23 +49,15 @@ export const DynamicZoneTemplate = ({
acceptLabel: "Yes, I'm sure!"
});
- const [templateToEdit, setTemplateToEdit] = useState(
- undefined
- );
-
const templates = field.settings?.templates || [];
const isFirst = index === 0;
const isLast = index === templates.length - 1;
const callbackDeps = [onChange, field, index, template.id];
- const onDialogClose = useCallback(() => {
- setTemplateToEdit(undefined);
- }, []);
-
const editTemplate = useCallback(() => {
- setTemplateToEdit(template);
- }, [template]);
+ onEditTemplate(template);
+ }, [template, onEditTemplate]);
const updateTemplate = useCallback(params => {
onChange(
@@ -102,7 +95,7 @@ export const DynamicZoneTemplate = ({
title={template.name}
description={template.description}
icon={icon ? : undefined}
- open={open}
+ defaultOpen={open}
actions={
<>
}
>
- {templateToEdit ? (
-
- ) : null}
-
{
+ {/**/}
{/**/}
{/**/}