diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ae62a39d11..d282b72015 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -11,11 +11,8 @@ "vscode": { // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "dbaeumer.vscode-eslint", - "Angular.ng-template", "cipchk.cssrem", - "huizhou.githd", - "supermaven.supermaven" + "huizhou.githd" ] } }, diff --git a/docker-compose.yml b/docker-compose.yml index 8aa3c34f51..872a496f99 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.0' services: activepieces: - image: ghcr.io/activepieces/activepieces:0.38.1 + image: ghcr.io/activepieces/activepieces:0.38.3 container_name: activepieces restart: unless-stopped ## Enable the following line if you already use AP_EXECUTION_MODE with SANDBOXED or old activepieces, checking the breaking change documentation for more info. diff --git a/docs/handbook/hiring/hiring.mdx b/docs/handbook/hiring/hiring.mdx index d2fdc08f93..93082b7eaf 100644 --- a/docs/handbook/hiring/hiring.mdx +++ b/docs/handbook/hiring/hiring.mdx @@ -13,7 +13,7 @@ Engineers are the majority of the Activepieces team, and we are always looking f We'll chat about your past experiences and how you design products. It's like having a friendly conversation where we reflect on what you've done before. - You'll do paid task for a short time (1-2 days). These tasks help us understand how well we work together. + You'll do open source task for one day. This open source contribution task help us understand how well we work together. diff --git a/package-lock.json b/package-lock.json index 36f8bbe0bd..45179aed02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "activepieces", - "version": "0.38.1", + "version": "0.38.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "activepieces", - "version": "0.38.1", + "version": "0.38.3", "dependencies": { "@activepieces/import-fresh-webpack": "3.3.0", "@actual-app/api": "6.8.1", diff --git a/package.json b/package.json index 1c0b34be44..be980631da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "activepieces", - "version": "0.38.1", - "rcVersion": "0.38.0-rc.0", + "version": "0.38.3", + "rcVersion": "0.39.0-rc.0", "scripts": { "prepare": "husky install", "serve:frontend": "nx serve react-ui", diff --git a/packages/ee/shared/src/lib/audit-events/index.ts b/packages/ee/shared/src/lib/audit-events/index.ts index 193c2a2a69..1773802e2f 100644 --- a/packages/ee/shared/src/lib/audit-events/index.ts +++ b/packages/ee/shared/src/lib/audit-events/index.ts @@ -19,6 +19,8 @@ export const ListAuditEventsRequest = Type.Object({ action: Type.Optional(Type.String()), projectId: Type.Optional(Type.Array(Type.String())), userId: Type.Optional(Type.String()), + createdBefore: Type.Optional(Type.String()), + createdAfter: Type.Optional(Type.String()), }); export type ListAuditEventsRequest = Static; diff --git a/packages/ee/shared/src/lib/git-repo/index.ts b/packages/ee/shared/src/lib/git-repo/index.ts index 9e235735a4..2ad387d570 100644 --- a/packages/ee/shared/src/lib/git-repo/index.ts +++ b/packages/ee/shared/src/lib/git-repo/index.ts @@ -12,14 +12,6 @@ export enum GitBranchType { DEVELOPMENT = 'DEVELOPMENT', } -export const GitProjectMappingState = Type.Object({ - flows: Type.Record(Type.String(), Type.Object({ - sourceId: Type.String(), - })), -}) - -export type GitProjectMappingState = Static - export const GitRepo = Type.Object({ ...BaseModelSchema, remoteUrl: Type.String(), @@ -28,7 +20,6 @@ export const GitRepo = Type.Object({ projectId: Type.String(), sshPrivateKey: Type.String(), slug: Type.String(), - mapping: Type.Optional(GitProjectMappingState), }) export type GitRepo = Static @@ -51,16 +42,6 @@ export const PushGitRepoRequest = Type.Object({ export type PushGitRepoRequest = Static -export const PullGitRepoFromProjectRequest = Type.Object({ - projectId: Type.String(), -}) -export type PullGitRepoFromProjectRequest = Static - -export const PullGitRepoRequest = Type.Object({ - dryRun: Type.Optional(Type.Boolean()), -}) -export type PullGitRepoRequest = Static - export const ConfigureRepoRequest = Type.Object({ projectId: Type.String({ minLength: 1, diff --git a/packages/ee/shared/src/lib/project/project-requests.ts b/packages/ee/shared/src/lib/project/project-requests.ts index fb9758b7da..6c6bf5092a 100644 --- a/packages/ee/shared/src/lib/project/project-requests.ts +++ b/packages/ee/shared/src/lib/project/project-requests.ts @@ -3,6 +3,7 @@ import { NotificationStatus, PiecesFilterType, SAFE_STRING_PATTERN } from "@acti export const UpdateProjectPlatformRequest = Type.Object({ notifyStatus: Type.Optional(Type.Enum(NotificationStatus)), + releasesEnabled: Type.Optional(Type.Boolean()), displayName: Type.Optional(Type.String({ pattern: SAFE_STRING_PATTERN, })), diff --git a/packages/pieces/community/file-helper/package.json b/packages/pieces/community/file-helper/package.json index 35e7d7399c..b18c73be82 100644 --- a/packages/pieces/community/file-helper/package.json +++ b/packages/pieces/community/file-helper/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-file-helper", - "version": "0.1.4" + "version": "0.1.5" } diff --git a/packages/pieces/community/file-helper/src/index.ts b/packages/pieces/community/file-helper/src/index.ts index ba06e50773..7d8cbd96d8 100644 --- a/packages/pieces/community/file-helper/src/index.ts +++ b/packages/pieces/community/file-helper/src/index.ts @@ -3,6 +3,7 @@ import { PieceCategory } from '@activepieces/shared'; import { readFileAction } from './lib/actions/read-file'; import { createFile } from './lib/actions/create-file'; import { changeFileEncoding } from './lib/actions/change-file-encoding'; +import { checkFileType} from './lib/actions/check-file-type'; export const filesHelper = createPiece({ displayName: 'Files Helper', @@ -12,6 +13,6 @@ export const filesHelper = createPiece({ logoUrl: 'https://cdn.activepieces.com/pieces/file-piece.svg', categories: [PieceCategory.CORE], authors: ['kishanprmr', 'MoShizzle', 'abuaboud', 'Seb-C'], - actions: [readFileAction, createFile, changeFileEncoding], + actions: [readFileAction, createFile, changeFileEncoding, checkFileType], triggers: [], }); diff --git a/packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts b/packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts new file mode 100644 index 0000000000..3ffdfb0257 --- /dev/null +++ b/packages/pieces/community/file-helper/src/lib/actions/check-file-type.ts @@ -0,0 +1,39 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { predefinedMimeTypes } from '../common/mimeTypes'; +import mime from 'mime-types'; + +export const checkFileType = createAction({ + name: 'checkFileType', + displayName: 'Check file type', + description: 'Check MIME type of a file and filter based on selected types', + props: { + file: Property.File({ + displayName: 'File to Check', + required: true, + }), + mimeTypes: Property.StaticDropdown({ + displayName: 'Select MIME Types', + required: true, + options: { + options: predefinedMimeTypes, + }, + description: 'Choose one or more MIME types to check against the file.', + }), + }, + async run(context) { + const file = context.propsValue.file; + + const selectedMimeTypes = context.propsValue.mimeTypes; + + // Determine the MIME type of the file + const fileType = file.extension ? mime.lookup(file.extension) || 'application/octet-stream' : 'application/octet-stream'; + + // Check if the file's MIME type matches any of the selected MIME types. + const isMatch = fileType && selectedMimeTypes.includes(fileType); + + return { + mimeType: fileType || 'unknown', + isMatch, + }; + }, +}); diff --git a/packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts b/packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts new file mode 100644 index 0000000000..f4840e8aaf --- /dev/null +++ b/packages/pieces/community/file-helper/src/lib/common/mimeTypes.ts @@ -0,0 +1,88 @@ +// Check out: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types +// Check out: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types +export const predefinedMimeTypes = [ + // important MIME types for Web developers + { label: 'Octet-stream', value: 'application/octet-stream' }, + // Textual Files + { label: 'Plain Text', value: 'text/plain' }, + { label: 'CSS Stylesheet', value: 'text/css' }, + { label: 'HTML Document', value: 'text/html' }, + { label: 'JavaScript', value: 'text/javascript' }, + { label: 'CSV File', value: 'text/csv' }, + { label: 'iCalendar Format', value: 'text/calendar' }, + // Image Types + { label: 'APNG Image', value: 'image/apng' }, + { label: 'AVIF Image', value: 'image/avif' }, + { label: 'GIF Image', value: 'image/gif' }, + { label: 'JPEG Image', value: 'image/jpeg' }, + { label: 'PNG Image', value: 'image/png' }, + { label: 'SVG Image', value: 'image/svg+xml' }, + { label: 'WebP Image', value: 'image/webp' }, + { label: 'BMP Image', value: 'image/bmp' }, + { label: 'Icon Format', value: 'image/vnd.microsoft.icon' }, + { label: 'TIFF Image', value: 'image/tiff' }, + // Audio Types + { label: 'AAC Audio', value: 'audio/aac' }, + { label: 'MP3 Audio', value: 'audio/mpeg' }, + { label: 'OGG Audio', value: 'audio/ogg' }, + { label: 'WAV Audio', value: 'audio/wav' }, + { label: 'FLAC Audio', value: 'audio/flac' }, + { label: 'MIDI Audio', value: 'audio/midi' }, + { label: 'WEBM Audio', value: 'audio/webm' }, + // Video Types + { label: 'MP4 Video', value: 'video/mp4' }, + { label: 'WebM Video', value: 'video/webm' }, + { label: 'OGG Video', value: 'video/ogg' }, + { label: 'AVI Video', value: 'video/x-msvideo' }, + { label: 'MPEG Video', value: 'video/mpeg' }, + { label: '3GPP Video', value: 'video/3gpp' }, + { label: '3GPP2 Video', value: 'video/3gpp2' }, + // Font Types + { label: 'EOT Font', value: 'application/vnd.ms-fontobject' }, + { label: 'OpenType Font', value: 'font/otf' }, + { label: 'TrueType Font', value: 'font/ttf' }, + { label: 'WOFF Font', value: 'font/woff' }, + { label: 'WOFF2 Font', value: 'font/woff2' }, + // Archive and Compressed Files + { label: 'BZip Archive', value: 'application/x-bzip' }, + { label: 'BZip2 Archive', value: 'application/x-bzip2' }, + { label: 'GZip Archive', value: 'application/gzip' }, + { label: 'RAR Archive', value: 'application/vnd.rar' }, + { label: 'TAR Archive', value: 'application/x-tar' }, + { label: 'ZIP Archive', value: 'application/zip' }, + { label: '7-Zip Archive', value: 'application/x-7z-compressed' }, + // Document Types + { label: 'AbiWord Document', value: 'application/x-abiword' }, + { label: 'PDF', value: 'application/pdf' }, + { label: 'Microsoft Word', value: 'application/msword' }, + { label: 'Microsoft Word (OpenXML)', value: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }, + { label: 'Microsoft Excel', value: 'application/vnd.ms-excel' }, + { label: 'Microsoft Excel (OpenXML)', value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }, + { label: 'Microsoft PowerPoint', value: 'application/vnd.ms-powerpoint' }, + { label: 'Microsoft PowerPoint (OpenXML)', value: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }, + { label: 'OpenDocument Presentation', value: 'application/vnd.oasis.opendocument.presentation' }, + { label: 'OpenDocument Spreadsheet', value: 'application/vnd.oasis.opendocument.spreadsheet' }, + { label: 'OpenDocument Text', value: 'application/vnd.oasis.opendocument.text' }, + { label: 'Rich Text Format', value: 'application/rtf' }, + { label: 'Electronic Publication (EPUB)', value: 'application/epub+zip' }, + { label: 'Amazon Kindle eBook', value: 'application/vnd.amazon.ebook' }, + { label: 'XUL', value: 'application/vnd.mozilla.xul+xml' }, + { label: 'PHP Script', value: 'application/x-httpd-php' }, + { label: 'Java Archive (JAR)', value: 'application/java-archive' }, + { label: 'Microsoft Visio', value: 'application/vnd.visio' }, + { label: 'Apple Installer Package', value: 'application/vnd.apple.installer+xml' }, + // Multipart + { label: 'Form Data (multipart/form-data)', value: 'multipart/form-data' }, + { label: 'Partial Content (multipart/byteranges)', value: 'multipart/byteranges' }, + // Other Important MIME Types + { label: 'JSON', value: 'application/json' }, + { label: 'JSON-LD', value: 'application/ld+json' }, + { label: 'XML', value: 'application/xml' }, + { label: 'XHTML', value: 'application/xhtml+xml' }, + { label: 'C-Shell Script', value: 'application/x-csh' }, + { label: 'Bourne Shell Script', value: 'application/x-sh' }, + { label: 'FreeARC Archive', value: 'application/x-freearc' }, + { label: 'CD Audio', value: 'application/x-cdf' }, + { label: 'MPEG Transport Stream', value: 'video/mp2t' }, + { label: 'Opus Audio in Ogg Container', value: 'audio/opus' }, +] \ No newline at end of file diff --git a/packages/pieces/community/google-sheets/package.json b/packages/pieces/community/google-sheets/package.json index 21304e80cd..c1f4b75a3c 100644 --- a/packages/pieces/community/google-sheets/package.json +++ b/packages/pieces/community/google-sheets/package.json @@ -1,4 +1,4 @@ { "name": "@activepieces/piece-google-sheets", - "version": "0.11.10" + "version": "0.11.11" } diff --git a/packages/pieces/community/google-sheets/src/lib/actions/find-rows.ts b/packages/pieces/community/google-sheets/src/lib/actions/find-rows.ts index d7640a8439..f7677135eb 100644 --- a/packages/pieces/community/google-sheets/src/lib/actions/find-rows.ts +++ b/packages/pieces/community/google-sheets/src/lib/actions/find-rows.ts @@ -58,7 +58,7 @@ export const findRowsAction = createAction({ const startingRow = propsValue.startingRow ?? 1; const numberOfRowsToReturn = propsValue.numberOfRows ?? 1; - let rows = await googleSheetsCommon.getGoogleSheetRows({ + const rows = await googleSheetsCommon.getGoogleSheetRows({ spreadsheetId: spreadSheetId, accessToken: auth.access_token, sheetId: sheetId, @@ -66,14 +66,6 @@ export const findRowsAction = createAction({ rowIndex_e: undefined, }); - // modify row number based on starting row number - rows = rows.map((row) => { - return { - row: row.row + startingRow - 1, - values: row.values, - }; - }); - const values = rows.map((row) => { return row.values; }); diff --git a/packages/pieces/community/google-sheets/src/lib/common/common.ts b/packages/pieces/community/google-sheets/src/lib/common/common.ts index 8aa4efa774..0e02c2407a 100644 --- a/packages/pieces/community/google-sheets/src/lib/common/common.ts +++ b/packages/pieces/community/google-sheets/src/lib/common/common.ts @@ -330,7 +330,9 @@ async function getGoogleSheetRows({ const headers = headerResponse.body.values[0]??[]; const headerCount = headers.length; - const labeledRowValues = transformWorkSheetValues(rowsResponse.body.values,0,headerCount); + const startingRow = rowIndex_s? rowIndex_s-1:0; + + const labeledRowValues = transformWorkSheetValues(rowsResponse.body.values,startingRow,headerCount); return labeledRowValues; } diff --git a/packages/react-ui/src/app/builder/builder-hooks.ts b/packages/react-ui/src/app/builder/builder-hooks.ts index e49d88a6c3..a124091ffb 100644 --- a/packages/react-ui/src/app/builder/builder-hooks.ts +++ b/packages/react-ui/src/app/builder/builder-hooks.ts @@ -36,6 +36,7 @@ import { CanvasShortcuts, CanvasShortcutsProps, } from './flow-canvas/context-menu/canvas-context-menu'; +import { STEP_CONTEXT_MENU_ATTRIBUTE } from './flow-canvas/utils/consts'; const flowUpdatesQueue = new PromiseQueue(); @@ -224,23 +225,33 @@ export const createBuilderStore = (initialState: BuilderInitialState) => }; }); }, - selectStepByName: (stepName: string) => { + selectStepByName: (selectedStep: string) => { set((state) => { - if (stepName === state.selectedStep) { + if (selectedStep === state.selectedStep) { return state; } + const selectedNodes = + isNil(selectedStep) || selectedStep === 'trigger' + ? [] + : [selectedStep]; + + const rightSidebar = + selectedStep === 'trigger' && + state.flowVersion.trigger.type === TriggerType.EMPTY + ? RightSideBarType.NONE + : RightSideBarType.PIECE_SETTINGS; + + const leftSidebar = !isNil(state.run) + ? LeftSideBarType.RUN_DETAILS + : LeftSideBarType.NONE; + return { - selectedStep: stepName, - rightSidebar: - stepName === 'trigger' && - state.flowVersion.trigger.type === TriggerType.EMPTY - ? RightSideBarType.NONE - : RightSideBarType.PIECE_SETTINGS, - leftSidebar: !isNil(state.run) - ? LeftSideBarType.RUN_DETAILS - : LeftSideBarType.NONE, + selectedStep, + rightSidebar, + leftSidebar, selectedBranchIndex: null, askAiButtonProps: null, + selectedNodes, }; }); }, @@ -460,9 +471,11 @@ export const createBuilderStore = (initialState: BuilderInitialState) => return { pieceSelectorStep: step, selectedStep: step ? step : state.selectedStep, - rightSidebar: step - ? RightSideBarType.PIECE_SETTINGS - : state.rightSidebar, + rightSidebar: + (step && step !== 'trigger') || + state.flowVersion.trigger.type !== TriggerType.EMPTY + ? RightSideBarType.PIECE_SETTINGS + : state.rightSidebar, }; }); }, @@ -483,19 +496,22 @@ const shortcutHandler = ( const shortcutActivated = Object.entries(CanvasShortcuts).find( ([_, shortcut]) => shortcut.shortcutKey?.toLowerCase() === event.key.toLowerCase() && - !!shortcut.withCtrl === event.ctrlKey && + !!( + shortcut.withCtrl === event.ctrlKey || + shortcut.withCtrl === event.metaKey + ) && !!shortcut.withShift === event.shiftKey, ); - if (shortcutActivated) { event.preventDefault(); event.stopPropagation(); + handlers[shortcutActivated[0] as keyof CanvasShortcutsProps](); } }; export const NODE_SELECTION_RECT_CLASS_NAME = 'react-flow__nodesselection-rect'; -export const isNodeSelectionActive = () => { +export const doesSelectionRectangleExist = () => { return document.querySelector(`.${NODE_SELECTION_RECT_CLASS_NAME}`) !== null; }; export const useHandleKeyPressOnCanvas = () => { @@ -520,24 +536,24 @@ export const useHandleKeyPressOnCanvas = () => { if ( e.target instanceof HTMLElement && (e.target === document.body || - e.target.classList.contains('react-flow__nodesselection-rect')) && + e.target.classList.contains('react-flow__nodesselection-rect') || + e.target.closest(`[data-${STEP_CONTEXT_MENU_ATTRIBUTE}]`)) && !readonly ) { - const doesNotContainTrigger = !selectedNodes.some( - (node) => node === flowVersion.trigger.name, + const selectedNodesWithoutTrigger = selectedNodes.filter( + (node) => node !== flowVersion.trigger.name, ); shortcutHandler(e, { Copy: () => { - if (doesNotContainTrigger && selectedNodes.length > 0) { - copySelectedNodes({ selectedNodes, flowVersion }); + if (selectedNodesWithoutTrigger.length > 0) { + copySelectedNodes({ + selectedNodes: selectedNodesWithoutTrigger, + flowVersion, + }); } }, Delete: () => { - if ( - isNodeSelectionActive() && - doesNotContainTrigger && - selectedNodes.length > 0 - ) { + if (selectedNodes.length > 0) { deleteSelectedNodes({ exitStepSettings, selectedStep, @@ -547,9 +563,9 @@ export const useHandleKeyPressOnCanvas = () => { } }, Skip: () => { - if (doesNotContainTrigger && selectedNodes.length > 0) { + if (selectedNodesWithoutTrigger.length > 0) { toggleSkipSelectedNodes({ - selectedNodes, + selectedNodes: selectedNodesWithoutTrigger, flowVersion, applyOperation, }); @@ -558,13 +574,19 @@ export const useHandleKeyPressOnCanvas = () => { Paste: () => { getActionsInClipboard().then((actions) => { if (actions.length > 0) { + const lastStep = [ + flowVersion.trigger, + ...flowStructureUtil.getAllNextActionsWithoutChildren( + flowVersion.trigger, + ), + ].at(-1)!.name; + const lastSelectedNode = + selectedNodes.length === 1 ? selectedNodes[0] : null; pasteNodes( actions, flowVersion, { - parentStepName: flowStructureUtil - .getAllNextActionsWithoutChildren(flowVersion.trigger) - .at(-1)!.name, + parentStepName: lastSelectedNode ?? lastStep, stepLocationRelativeToParent: StepLocationRelativeToParent.AFTER, }, diff --git a/packages/react-ui/src/app/builder/flow-canvas/bulk-actions.ts b/packages/react-ui/src/app/builder/flow-canvas/bulk-actions.ts index eefd20539d..0c9e21366e 100644 --- a/packages/react-ui/src/app/builder/flow-canvas/bulk-actions.ts +++ b/packages/react-ui/src/app/builder/flow-canvas/bulk-actions.ts @@ -94,10 +94,11 @@ export function pasteNodes( export function getLastLocationAsPasteLocation( flowVersion: FlowVersion, ): PasteLocation { - const nextActions = flowStructureUtil.getAllNextActionsWithoutChildren( + const firstLevelParents = [ flowVersion.trigger, - ); - const lastAction = nextActions[nextActions.length - 1]; + ...flowStructureUtil.getAllNextActionsWithoutChildren(flowVersion.trigger), + ]; + const lastAction = firstLevelParents[firstLevelParents.length - 1]; return { parentStepName: lastAction.name, stepLocationRelativeToParent: StepLocationRelativeToParent.AFTER, diff --git a/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx b/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx index f955c9e930..ccd2fda5bf 100644 --- a/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx +++ b/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu-content.tsx @@ -35,7 +35,11 @@ import { toggleSkipSelectedNodes, } from '../bulk-actions'; -import { CanvasContextMenuProps, CanvasShortcuts } from './canvas-context-menu'; +import { + CanvasContextMenuProps, + CanvasShortcuts, + ContextMenuType, +} from './canvas-context-menu'; const ShortcutWrapper = ({ children, @@ -61,27 +65,53 @@ export const CanvasContextMenuContent = ({ readonly, actionsToPaste, setPieceSelectorStep, + contextMenuType, }: CanvasContextMenuProps) => { const disabled = selectedNodes.length === 0; const areAllStepsSkipped = selectedNodes.every( (node) => !!(flowStructureUtil.getStep(node, flowVersion.trigger) as Action)?.skip, ); - const doesNotContainTrigger = !selectedNodes.some( + const doSelectedNodesIncludeTrigger = selectedNodes.some( (node) => node === flowVersion.trigger.name, ); - const showPasteAfterLastStep = !readonly && selectedNodes.length === 0; const disabledPaste = actionsToPaste.length === 0; const firstSelectedStep = flowStructureUtil.getStep( selectedNodes[0], flowVersion.trigger, ); - + const showPasteAfterLastStep = + !readonly && contextMenuType === ContextMenuType.CANVAS; const showPasteAsFirstLoopAction = selectedNodes.length === 1 && - firstSelectedStep?.type === ActionType.LOOP_ON_ITEMS; + firstSelectedStep?.type === ActionType.LOOP_ON_ITEMS && + !readonly && + contextMenuType === ContextMenuType.STEP; const showPasteAsBranchChild = - selectedNodes.length === 1 && firstSelectedStep?.type === ActionType.ROUTER; + selectedNodes.length === 1 && + firstSelectedStep?.type === ActionType.ROUTER && + !readonly && + contextMenuType === ContextMenuType.STEP; + const showPasteAfterCurrentStep = + selectedNodes.length === 1 && + !readonly && + contextMenuType === ContextMenuType.STEP; + const showReplace = + selectedNodes.length === 1 && + !readonly && + contextMenuType === ContextMenuType.STEP; + const showCopy = + !doSelectedNodesIncludeTrigger && contextMenuType === ContextMenuType.STEP; + const showDuplicate = + selectedNodes.length === 1 && + !doSelectedNodesIncludeTrigger && + contextMenuType === ContextMenuType.STEP && + !readonly; + const showSkip = + !doSelectedNodesIncludeTrigger && + contextMenuType === ContextMenuType.STEP && + !readonly; + const showDelete = !readonly && contextMenuType === ContextMenuType.STEP; const duplicateStep = () => { applyOperation( @@ -94,10 +124,9 @@ export const CanvasContextMenuContent = ({ () => toast(UNSAVED_CHANGES_TOAST), ); }; - const showPasteAfterCurrentStep = selectedNodes.length === 1; return ( <> - {selectedNodes.length === 1 && !readonly && ( + {showReplace && ( { @@ -108,7 +137,7 @@ export const CanvasContextMenuContent = ({ {t('Replace')} )} - {doesNotContainTrigger && ( + {showCopy && ( { @@ -121,209 +150,201 @@ export const CanvasContextMenuContent = ({ )} - {!readonly && ( - <> - {selectedNodes.length === 1 && doesNotContainTrigger && ( - - {t('Duplicate')} - - )} + <> + {showDuplicate && ( + + {t('Duplicate')} + + )} - {doesNotContainTrigger && ( - { - toggleSkipSelectedNodes({ - selectedNodes, - flowVersion, - applyOperation, - }); - }} - > - - {areAllStepsSkipped ? ( - - ) : ( - - )} - {t(areAllStepsSkipped ? 'Unskip' : 'Skip')} - - - )} - {(showPasteAfterLastStep || - showPasteAsFirstLoopAction || - showPasteAsBranchChild || - showPasteAfterCurrentStep) && ( - - )} + {showSkip && ( + { + toggleSkipSelectedNodes({ + selectedNodes, + flowVersion, + applyOperation, + }); + }} + > + + {areAllStepsSkipped ? ( + + ) : ( + + )} + {t(areAllStepsSkipped ? 'Unskip' : 'Skip')} + + + )} + {(showPasteAsFirstLoopAction || + showPasteAsBranchChild || + showPasteAfterCurrentStep) && ( + + )} - {showPasteAfterLastStep && ( - { - const pasteLocation = - getLastLocationAsPasteLocation(flowVersion); - if (pasteLocation) { - pasteNodes( - actionsToPaste, - flowVersion, - pasteLocation, - applyOperation, - ); - } - }} - > - - {' '} - {t('Paste After Last Step')} - - - )} - - {showPasteAsFirstLoopAction && ( - { + {showPasteAfterLastStep && ( + { + const pasteLocation = getLastLocationAsPasteLocation(flowVersion); + if (pasteLocation) { pasteNodes( actionsToPaste, flowVersion, - { - parentStepName: selectedNodes[0], - stepLocationRelativeToParent: - StepLocationRelativeToParent.INSIDE_LOOP, - }, + pasteLocation, applyOperation, ); - }} - className="flex items-center gap-2" - > - {' '} - {t('Paste Inside Loop')} - - )} + } + }} + className="flex items-center gap-2" + > + {' '} + {t('Paste After Last Step')} + + )} - {showPasteAfterCurrentStep && ( - { - pasteNodes( - actionsToPaste, - flowVersion, - { - parentStepName: selectedNodes[0], - stepLocationRelativeToParent: - StepLocationRelativeToParent.AFTER, - }, - applyOperation, - ); - }} - className="flex items-center gap-2" - > - {' '} - {t('Paste After')} - - )} + {showPasteAsFirstLoopAction && ( + { + pasteNodes( + actionsToPaste, + flowVersion, + { + parentStepName: selectedNodes[0], + stepLocationRelativeToParent: + StepLocationRelativeToParent.INSIDE_LOOP, + }, + applyOperation, + ); + }} + className="flex items-center gap-2" + > + {' '} + {t('Paste Inside Loop')} + + )} - {showPasteAsBranchChild && !disabledPaste && ( - - - {' '} - {t('Paste Inside...')} - - - {firstSelectedStep && - firstSelectedStep.settings.branches.map( - (branch, branchIndex) => ( - { - pasteNodes( - actionsToPaste, - flowVersion, - { - parentStepName: selectedNodes[0], - stepLocationRelativeToParent: - StepLocationRelativeToParent.INSIDE_BRANCH, - branchIndex, - }, - applyOperation, - ); - }} - > - {branch.branchName} - - ), - )} - { - applyOperation( - { - type: FlowOperationType.ADD_BRANCH, - request: { - stepName: firstSelectedStep.name, - branchIndex: - firstSelectedStep.settings.branches.length - 1, - branchName: `Branch ${firstSelectedStep.settings.branches.length}`, - }, - }, - () => {}, - ); - pasteNodes( - actionsToPaste, - flowVersion, - { - parentStepName: firstSelectedStep.name, - stepLocationRelativeToParent: - StepLocationRelativeToParent.INSIDE_BRANCH, - branchIndex: - firstSelectedStep.settings.branches.length - 1, - }, - applyOperation, - ); - }} - > - + {t('New Branch')} - - - - )} + {showPasteAfterCurrentStep && ( + { + pasteNodes( + actionsToPaste, + flowVersion, + { + parentStepName: selectedNodes[0], + stepLocationRelativeToParent: + StepLocationRelativeToParent.AFTER, + }, + applyOperation, + ); + }} + className="flex items-center gap-2" + > + {' '} + {t('Paste After')} + + )} - {showPasteAsBranchChild && disabledPaste && ( - + {showPasteAsBranchChild && !disabledPaste && ( + + {' '} - {t('Paste Inside Branch')} - - )} - {doesNotContainTrigger && !readonly && ( - <> - + {t('Paste Inside...')} + + + {firstSelectedStep && + firstSelectedStep.settings.branches.map( + (branch, branchIndex) => ( + { + pasteNodes( + actionsToPaste, + flowVersion, + { + parentStepName: selectedNodes[0], + stepLocationRelativeToParent: + StepLocationRelativeToParent.INSIDE_BRANCH, + branchIndex, + }, + applyOperation, + ); + }} + > + {branch.branchName} + + ), + )} { - deleteSelectedNodes({ - selectedNodes, + applyOperation( + { + type: FlowOperationType.ADD_BRANCH, + request: { + stepName: firstSelectedStep.name, + branchIndex: + firstSelectedStep.settings.branches.length - 1, + branchName: `Branch ${firstSelectedStep.settings.branches.length}`, + }, + }, + () => {}, + ); + pasteNodes( + actionsToPaste, + flowVersion, + { + parentStepName: firstSelectedStep.name, + stepLocationRelativeToParent: + StepLocationRelativeToParent.INSIDE_BRANCH, + branchIndex: + firstSelectedStep.settings.branches.length - 1, + }, applyOperation, - selectedStep, - exitStepSettings, - }); + ); }} > - - {' '} -
{t('Delete')}
-
+ + {t('New Branch')}
- - )} - - )} +
+ + )} + + {showPasteAsBranchChild && disabledPaste && ( + + {' '} + {t('Paste Inside Branch')} + + )} + {showDelete && ( + <> + + { + deleteSelectedNodes({ + selectedNodes, + applyOperation, + selectedStep, + exitStepSettings, + }); + }} + > + + {' '} +
{t('Delete')}
+
+
+ + )} + ); }; diff --git a/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu.tsx b/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu.tsx index 41ada04525..0490a424b9 100644 --- a/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu.tsx +++ b/packages/react-ui/src/app/builder/flow-canvas/context-menu/canvas-context-menu.tsx @@ -36,7 +36,10 @@ export const CanvasShortcuts: CanvasShortcutsProps = { shortcutKey: 'e', }, }; - +export enum ContextMenuType { + CANVAS = 'CANVAS', + STEP = 'STEP', +} export type CanvasContextMenuProps = Pick< BuilderState, | 'applyOperation' @@ -49,6 +52,7 @@ export type CanvasContextMenuProps = Pick< > & { children?: React.ReactNode; actionsToPaste: Action[]; + contextMenuType: ContextMenuType; }; export const CanvasContextMenu = ({ selectedNodes, @@ -60,30 +64,24 @@ export const CanvasContextMenu = ({ readonly, setPieceSelectorStep, actionsToPaste, + contextMenuType, }: CanvasContextMenuProps) => { - const doesNotContainTrigger = !selectedNodes.some( - (node) => node === flowVersion.trigger.name, - ); - return ( {children} - {!doesNotContainTrigger && readonly ? ( - <> - ) : ( - - - - )} + + + ); }; diff --git a/packages/react-ui/src/app/builder/flow-canvas/index.tsx b/packages/react-ui/src/app/builder/flow-canvas/index.tsx index 71dc6fc73a..abc855d722 100644 --- a/packages/react-ui/src/app/builder/flow-canvas/index.tsx +++ b/packages/react-ui/src/app/builder/flow-canvas/index.tsx @@ -9,11 +9,16 @@ import { useKeyPress, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { usePrevious } from 'react-use'; import { - Action, ActionType, flowStructureUtil, FlowVersion, @@ -24,14 +29,17 @@ import { import { flowRunUtils } from '../../../features/flow-runs/lib/flow-run-utils'; import { - isNodeSelectionActive, + doesSelectionRectangleExist, NODE_SELECTION_RECT_CLASS_NAME, useBuilderStateContext, useHandleKeyPressOnCanvas, usePasteActionsInClipboard, } from '../builder-hooks'; -import { CanvasContextMenu } from './context-menu/canvas-context-menu'; +import { + CanvasContextMenu, + ContextMenuType, +} from './context-menu/canvas-context-menu'; import { FlowDragLayer } from './flow-drag-layer'; import { flowUtilConsts, STEP_CONTEXT_MENU_ATTRIBUTE } from './utils/consts'; import { flowCanvasUtils } from './utils/flow-canvas-utils'; @@ -94,6 +102,7 @@ export const FlowCanvas = React.memo( exitStepSettings, panningMode, setPieceSelectorStep, + selectStepByName, ] = useBuilderStateContext((state) => { return [ state.allowCanvasPanning, @@ -107,6 +116,7 @@ export const FlowCanvas = React.memo( state.exitStepSettings, state.panningMode, state.setPieceSelectorStep, + state.selectStepByName, ]; }); const { actionsToPaste, fetchClipboardOperations } = @@ -162,17 +172,23 @@ export const FlowCanvas = React.memo( resizeObserver.disconnect(); }; }, [setViewport, getViewport]); - const onSelectionChange = useCallback( (ev: OnSelectionChangeParams) => { - setSelectedNodes(ev.nodes.map((n) => n.id)); + const selectedNodes = ev.nodes.map((n) => n.id); + if (selectedNodes.length === 0 && selectedStep) { + selectedNodes.push(selectedStep); + } + setSelectedNodes(selectedNodes); }, - [setSelectedNodes], + [setSelectedNodes, selectedStep], ); const graphKey = createGraphKey(flowVersion); const graph = useMemo(() => { return flowCanvasUtils.convertFlowVersionToGraph(flowVersion); }, [graphKey]); + const [contextMenuType, setContextMenuType] = useState( + ContextMenuType.CANVAS, + ); const onContextMenu = useCallback( (ev: React.MouseEvent) => { fetchClipboardOperations(); @@ -186,21 +202,31 @@ export const FlowCanvas = React.memo( const stepName = stepElement?.getAttribute( `data-${STEP_CONTEXT_MENU_ATTRIBUTE}`, ); - setSelectedNodes( - isNodeSelectionActive() && !stepElement - ? selectedNodes - : stepName - ? [stepName] - : [], - ); - if (isNodeSelectionActive() && stepElement) { + + if (stepElement && stepName) { + selectStepByName(stepName); + storeApi.getState().addSelectedNodes([stepName]); + } + if ( + stepElement || + ev.target.classList.contains(NODE_SELECTION_RECT_CLASS_NAME) + ) { + setContextMenuType(ContextMenuType.STEP); + } else { + setContextMenuType(ContextMenuType.CANVAS); + } + + if ( + doesSelectionRectangleExist() && + !ev.target.classList.contains(NODE_SELECTION_RECT_CLASS_NAME) + ) { document .querySelector(`.${NODE_SELECTION_RECT_CLASS_NAME}`) ?.remove(); } } }, - [setSelectedNodes, selectedNodes], + [setSelectedNodes, selectedNodes, doesSelectionRectangleExist], ); const handleKeyDown = useHandleKeyPressOnCanvas(); @@ -213,9 +239,15 @@ export const FlowCanvas = React.memo( const inGrabPanningMode = !isShiftKeyPressed && panningMode === 'grab'; const onSelectionEnd = useCallback(() => { + if ( + !storeApi.getState().userSelectionActive || + doesSelectionRectangleExist() + ) { + return; + } const selectedSteps = selectedNodes.map((node) => flowStructureUtil.getStepOrThrow(node, flowVersion.trigger), - ) as Action[]; + ); selectedSteps.forEach((step) => { if ( step.type === ActionType.LOOP_ON_ITEMS || @@ -223,18 +255,20 @@ export const FlowCanvas = React.memo( ) { const childrenNotSelected = flowStructureUtil .getAllChildSteps(step) - .filter((c) => - isNil(selectedNodes.find((n) => n === c.name)), - ) as Action[]; + .filter((c) => isNil(selectedNodes.find((n) => n === c.name))); selectedSteps.push(...childrenNotSelected); } }); + const step = selectedStep + ? flowStructureUtil.getStep(selectedStep, flowVersion.trigger) + : null; + if (selectedNodes.length === 0 && step) { + selectedSteps.push(step); + } storeApi .getState() .addSelectedNodes(selectedSteps.map((step) => step.name)); - setSelectedNodes(selectedSteps.map((step) => step.name)); - }, [selectedNodes, storeApi, setSelectedNodes]); - + }, [selectedNodes, storeApi, selectedStep]); return (
self.findIndex((t) => t.key === el.key) === index, + ) + .toReversed(); const haveValuesChangedFromOutside = - valuesArrayRef.current.length !== valuesArray.length || + valuesArrayRefUnique.length !== valuesArray.length || valuesArray.reduce((acc, _, index) => { return ( acc || - valuesArrayRef.current[index].key !== valuesArray[index].key || - valuesArrayRef.current[index].value !== valuesArray[index].value + valuesArrayRefUnique[index].key !== valuesArray[index].key || + valuesArrayRefUnique[index].value !== valuesArray[index].value ); }, false); diff --git a/packages/react-ui/src/app/components/dashboard-container.tsx b/packages/react-ui/src/app/components/dashboard-container.tsx index 134327ecfa..4e7601b636 100644 --- a/packages/react-ui/src/app/components/dashboard-container.tsx +++ b/packages/react-ui/src/app/components/dashboard-container.tsx @@ -1,11 +1,12 @@ import { t } from 'i18next'; -import { AlertCircle, Link2, Logs, Workflow, Wrench } from 'lucide-react'; +import { AlertCircle, Box, Link2, Logs, Workflow, Wrench } from 'lucide-react'; import { Navigate } from 'react-router-dom'; import { useEmbedding } from '@/components/embed-provider'; import { issueHooks } from '@/features/issues/hooks/issue-hooks'; import { useAuthorization } from '@/hooks/authorization-hooks'; import { platformHooks } from '@/hooks/platform-hooks'; +import { projectHooks } from '@/hooks/project-hooks'; import { isNil, Permission } from '@activepieces/shared'; import { authenticationSession } from '../../lib/authentication-session'; @@ -22,6 +23,7 @@ export function DashboardContainer({ children }: DashboardContainerProps) { const { data: showIssuesNotification } = issueHooks.useIssuesNotification( platform.flowIssuesEnabled, ); + const { project } = projectHooks.useCurrentProject(); const { embedState } = useEmbedding(); const currentProjectId = authenticationSession.getProjectId(); @@ -64,6 +66,12 @@ export function DashboardContainer({ children }: DashboardContainerProps) { showInEmbed: true, hasPermission: checkAccess(Permission.READ_APP_CONNECTION), }, + { + to: authenticationSession.appendProjectRoutePrefix('/releases'), + label: t('Releases'), + icon: Box, + hasPermission: project.releasesEnabled, + }, { to: authenticationSession.appendProjectRoutePrefix('/settings/general'), label: t('Settings'), diff --git a/packages/react-ui/src/app/components/flow-actions-menu.tsx b/packages/react-ui/src/app/components/flow-actions-menu.tsx index f15a65ad26..4b7377be51 100644 --- a/packages/react-ui/src/app/components/flow-actions-menu.tsx +++ b/packages/react-ui/src/app/components/flow-actions-menu.tsx @@ -73,7 +73,7 @@ const FlowActionMenu: React.FC = ({ const openNewWindow = useNewWindow(); const { gitSync } = gitSyncHooks.useGitSync( authenticationSession.getProjectId()!, - platform.gitSyncEnabled, + platform.environmentsEnabled, ); const { checkAccess } = useAuthorization(); const userHasPermissionToUpdateFlow = checkAccess(Permission.WRITE_FLOW); @@ -252,7 +252,7 @@ const FlowActionMenu: React.FC = ({ {isDevelopmentBranch && (
{t( - 'You are on a development branch, this will not delete the flow from the remote repository.', + 'You are on a development branch, this will also delete the flow from the remote repository.', )}
)} diff --git a/packages/react-ui/src/app/components/project-settings-layout.tsx b/packages/react-ui/src/app/components/project-settings-layout.tsx index 308c549091..69e4bb9a3e 100644 --- a/packages/react-ui/src/app/components/project-settings-layout.tsx +++ b/packages/react-ui/src/app/components/project-settings-layout.tsx @@ -62,9 +62,9 @@ export default function ProjectSettingsLayout({ hasPermission: checkAccess(Permission.READ_ALERT), }, { - title: t('Git Sync'), + title: t('Environments'), href: authenticationSession.appendProjectRoutePrefix( - '/settings/git-sync', + '/settings/environments', ), icon: , hasPermission: checkAccess(Permission.READ_GIT_REPO), diff --git a/packages/react-ui/src/app/components/request-trial.tsx b/packages/react-ui/src/app/components/request-trial.tsx index d24297d5a7..58e1cc06ae 100644 --- a/packages/react-ui/src/app/components/request-trial.tsx +++ b/packages/react-ui/src/app/components/request-trial.tsx @@ -60,7 +60,7 @@ export type FeatureKey = | 'API' | 'SSO' | 'AUDIT_LOGS' - | 'GIT_SYNC' + | 'ENVIRONMENT' | 'ISSUES' | 'ANALYTICS' | 'ALERTS' diff --git a/packages/react-ui/src/app/router/index.tsx b/packages/react-ui/src/app/router/index.tsx index a6a7f15fba..75097a2e5d 100644 --- a/packages/react-ui/src/app/router/index.tsx +++ b/packages/react-ui/src/app/router/index.tsx @@ -56,11 +56,12 @@ import { GlobalConnectionsTable } from '../routes/platform/setup/connections'; import { LicenseKeyPage } from '../routes/platform/setup/license-key'; import TemplatesPage from '../routes/platform/setup/templates'; import UsersPage from '../routes/platform/users'; +import { ProjectReleasesPage } from '../routes/project-release'; import { FlowRunPage } from '../routes/runs/id'; import AlertsPage from '../routes/settings/alerts'; import AppearancePage from '../routes/settings/appearance'; +import { EnvironmentPage } from '../routes/settings/environment'; import GeneralPage from '../routes/settings/general'; -import { GitSyncPage } from '../routes/settings/git-sync'; import TeamPage from '../routes/settings/team'; import { SignInPage } from '../routes/sign-in'; import { SignUpPage } from '../routes/sign-up'; @@ -200,6 +201,16 @@ const routes = [ ), }), + ...ProjectRouterWrapper({ + path: '/releases', + element: ( + + + + + + ), + }), ...ProjectRouterWrapper({ path: '/plans', element: ( @@ -330,13 +341,13 @@ const routes = [ }, ...ProjectRouterWrapper({ - path: '/settings/git-sync', + path: '/settings/environments', element: ( - - + + diff --git a/packages/react-ui/src/app/routes/flows/index.tsx b/packages/react-ui/src/app/routes/flows/index.tsx index 3cf6c4eb5b..206178c462 100644 --- a/packages/react-ui/src/app/routes/flows/index.tsx +++ b/packages/react-ui/src/app/routes/flows/index.tsx @@ -95,7 +95,7 @@ const FlowsPage = () => { const { platform } = platformHooks.useCurrentPlatform(); const { gitSync } = gitSyncHooks.useGitSync( authenticationSession.getProjectId()!, - platform.gitSyncEnabled, + platform.environmentsEnabled, ); const userHasPermissionToUpdateFlow = checkAccess(Permission.WRITE_FLOW); const userHasPermissionToPushToGit = checkAccess(Permission.WRITE_GIT_REPO); diff --git a/packages/react-ui/src/app/routes/platform/analytics/task-usage.tsx b/packages/react-ui/src/app/routes/platform/analytics/task-usage.tsx index 871b64b768..7ed26d7740 100644 --- a/packages/react-ui/src/app/routes/platform/analytics/task-usage.tsx +++ b/packages/react-ui/src/app/routes/platform/analytics/task-usage.tsx @@ -1,6 +1,7 @@ 'use client'; import dayjs from 'dayjs'; +import { t } from 'i18next'; import * as React from 'react'; import { DateRange } from 'react-day-picker'; import { BarChart, CartesianGrid, XAxis, Bar } from 'recharts'; @@ -11,7 +12,7 @@ import { ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart'; -import { DatePickerWithRange } from '@/components/ui/date-picker-range'; +import { DateTimePickerWithRange } from '@/components/ui/date-time-picker-range'; import { Skeleton } from '@/components/ui/skeleton'; import { AnalyticsReportResponse } from '@activepieces/shared'; @@ -54,10 +55,10 @@ export function TaskUsage({ report }: TaskUsageProps) { <>
-
Executed Tasks
-

Showing total executed tasks for specified time range

+
{t('Executed Tasks')}
+

{t('Showing total executed tasks for specified time range')}

- ), cell: ({ row }) => { - return 'project' in row.original.data ? ( -
- {row.original.data.project?.displayName} -
+ return row.original.projectId && + 'project' in row.original.data ? ( + +
+ {row.original.data.project?.displayName} +
+ ) : (
{t('N/A')}
); diff --git a/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx b/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx index 2a5b51e096..007db4f4d3 100644 --- a/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx +++ b/packages/react-ui/src/app/routes/platform/setup/license-key/index.tsx @@ -34,7 +34,7 @@ import { ApEdition, ApFlagId, isNil } from '@activepieces/shared'; import { ActivateLicenseDialog } from './activate-license-dialog'; const LICENSE_PROPS_MAP = { - gitSyncEnabled: 'Team Collaboration via Git', + environmentEnabled: 'Team Collaboration via Git', analyticsEnabled: 'Analytics', auditLogEnabled: 'Audit Log', embeddingEnabled: 'Embedding', diff --git a/packages/react-ui/src/app/routes/project-release/apply-plan.tsx b/packages/react-ui/src/app/routes/project-release/apply-plan.tsx new file mode 100644 index 0000000000..990954f505 --- /dev/null +++ b/packages/react-ui/src/app/routes/project-release/apply-plan.tsx @@ -0,0 +1,106 @@ +import { useMutation } from '@tanstack/react-query'; +import { t } from 'i18next'; +import { useState, ReactNode } from 'react'; + +import { Button, ButtonProps } from '@/components/ui/button'; +import { INTERNAL_ERROR_TOAST, useToast } from '@/components/ui/use-toast'; +import { ConnectGitDialog } from '@/features/git-sync/components/connect-git-dialog'; +import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks'; +import { projectReleaseApi } from '@/features/project-version/lib/project-release-api'; +import { authenticationSession } from '@/lib/authentication-session'; +import { + DiffReleaseRequest, + isNil, + ProjectReleaseType, +} from '@activepieces/shared'; + +import { CreateReleaseDialog } from './create-release-dialog'; + +type ApplyButtonProps = ButtonProps & { + request: DiffReleaseRequest; + children: ReactNode; + onSuccess: () => void; + defaultName?: string; +}; + +export const ApplyButton = ({ + request, + children, + onSuccess, + defaultName, + ...props +}: ApplyButtonProps) => { + const { toast } = useToast(); + const projectId = authenticationSession.getProjectId()!; + const { gitSync } = gitSyncHooks.useGitSync(projectId, !isNil(projectId)); + const [dialogOpen, setDialogOpen] = useState(false); + const [syncPlan, setSyncPlan] = useState(null); + const [loadingRequestId, setLoadingRequestId] = useState(null); + + const { mutate: loadSyncPlan } = useMutation({ + mutationFn: (request: DiffReleaseRequest) => + projectReleaseApi.diff(request), + onSuccess: (plan) => { + if (!plan.operations || plan.operations.length === 0) { + toast({ + title: t('No Changes Found'), + description: t('There are no differences to apply'), + variant: 'default', + }); + setLoadingRequestId(null); + return; + } + setSyncPlan(plan); + setDialogOpen(true); + setLoadingRequestId(null); + }, + onError: () => { + toast(INTERNAL_ERROR_TOAST); + setLoadingRequestId(null); + }, + }); + + const [gitDialogOpen, setGitDialogOpen] = useState(false); + const showGitDialog = + isNil(gitSync) && request.type === ProjectReleaseType.GIT; + const requestId = JSON.stringify(request); + const isLoading = loadingRequestId === requestId; + + return ( + <> + + + {gitDialogOpen ? ( + + ) : ( + dialogOpen && ( + + ) + )} + + ); +}; diff --git a/packages/react-ui/src/app/routes/project-release/create-release-dialog/index.tsx b/packages/react-ui/src/app/routes/project-release/create-release-dialog/index.tsx new file mode 100644 index 0000000000..713653abc8 --- /dev/null +++ b/packages/react-ui/src/app/routes/project-release/create-release-dialog/index.tsx @@ -0,0 +1,246 @@ +import { zodResolver } from '@hookform/resolvers/zod'; +import { useMutation } from '@tanstack/react-query'; +import { t } from 'i18next'; +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; + +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { INTERNAL_ERROR_TOAST, toast } from '@/components/ui/use-toast'; +import { gitSyncHooks } from '@/features/git-sync/lib/git-sync-hooks'; +import { projectReleaseApi } from '@/features/project-version/lib/project-release-api'; +import { platformHooks } from '@/hooks/platform-hooks'; +import { authenticationSession } from '@/lib/authentication-session'; +import { ProjectSyncPlan } from '@activepieces/ee-shared'; +import { DiffReleaseRequest, ProjectReleaseType } from '@activepieces/shared'; + +import { OperationChange } from './operation-change'; + +type CreateReleaseDialogProps = { + open: boolean; + setOpen: (open: boolean) => void; + refetch: () => void; + diffRequest: DiffReleaseRequest; + plan: ProjectSyncPlan | undefined; + defaultName?: string; +}; + +const formSchema = z.object({ + name: z.string().min(1, t('Name is required')), + description: z.string(), +}); + +type FormData = z.infer; + +const CreateReleaseDialog = ({ + open, + setOpen, + refetch, + plan, + defaultName = '', + diffRequest, +}: CreateReleaseDialogProps) => { + const { platform } = platformHooks.useCurrentPlatform(); + const { gitSync } = gitSyncHooks.useGitSync( + authenticationSession.getProjectId()!, + platform.environmentsEnabled, + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: defaultName, + description: '', + }, + }); + + const { mutate: applyChanges, isPending } = useMutation({ + mutationFn: async () => { + switch (diffRequest.type) { + case ProjectReleaseType.GIT: + if (!gitSync) { + throw new Error('Git sync is not connected'); + } + await projectReleaseApi.create({ + name: form.getValues('name'), + description: form.getValues('description'), + selectedFlowsIds: Array.from(selectedChanges), + repoId: gitSync.id, + type: diffRequest.type, + }); + break; + case ProjectReleaseType.PROJECT: + if (!diffRequest.targetProjectId) { + throw new Error('Project ID is required'); + } + await projectReleaseApi.create({ + name: form.getValues('name'), + description: form.getValues('description'), + selectedFlowsIds: Array.from(selectedChanges), + targetProjectId: diffRequest.targetProjectId, + type: diffRequest.type, + }); + break; + case ProjectReleaseType.ROLLBACK: + await projectReleaseApi.create({ + name: form.getValues('name'), + description: form.getValues('description'), + selectedFlowsIds: Array.from(selectedChanges), + projectReleaseId: diffRequest.projectReleaseId, + type: diffRequest.type, + }); + break; + } + }, + onSuccess: () => { + refetch(); + setOpen(false); + }, + onError: (error) => { + console.error(error); + toast(INTERNAL_ERROR_TOAST); + }, + }); + + const [selectedChanges, setSelectedChanges] = useState>( + new Set(plan?.operations.map((op) => op.flow.id) || []), + ); + + const handleSelectAll = (checked: boolean) => { + if (!plan) return; + setSelectedChanges( + new Set(checked ? plan.operations.map((op) => op.flow.id) : []), + ); + }; + + return ( + { + if (newOpenState) { + form.reset({ + name: '', + description: '', + }); + } + setOpen(newOpenState); + }} + > + + + + {diffRequest.type === ProjectReleaseType.GIT + ? t('Create Git Release') + : diffRequest.type === ProjectReleaseType.PROJECT + ? t('Create Project Release') + : t('Rollback Release')} + + +
+
+ + + {form.formState.errors.name && ( +

+ {form.formState.errors.name.message} +

+ )} +
+
+ +