Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslation } from "react-i18next";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import { usePermissions } from "@/contexts/permissionsContext";
import DeploymentStepperModal from "@/pages/MainPage/pages/deploymentsPage/components/deployment-stepper-modal";
import { useNavigateToTest } from "@/pages/MainPage/pages/deploymentsPage/hooks/use-navigate-to-test";
import { useUtilityStore } from "@/stores/utilityStore";
Expand Down Expand Up @@ -28,6 +29,10 @@ function DeployButtonInner() {

const navigateToTest = useNavigateToTest();

const { can } = usePermissions();
// Deploying provisions and runs the flow on an environment → gate on execute.
const canDeploy = can(currentFlowId, "execute");

return (
<>
<button
Expand All @@ -37,7 +42,8 @@ function DeployButtonInner() {
isPreparingDeploy ||
choiceDialogOpen ||
deployModalOpen ||
!currentFlowId
!currentFlowId ||
!canDeploy
}
className="relative inline-flex h-8 items-center justify-start gap-1.5 rounded bg-primary px-2 text-sm font-normal text-primary-foreground hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
data-testid="deploy-btn-flow"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Switch } from "@/components/ui/switch";
import { usePermissions } from "@/contexts/permissionsContext";
import { usePatchUpdateFlow } from "@/controllers/API/queries/flows/use-patch-update-flow";
import { CustomLink } from "@/customization/components/custom-link";
import { ENABLE_PUBLISH, ENABLE_WIDGET } from "@/customization/feature-flags";
Expand Down Expand Up @@ -50,6 +51,11 @@ export default function PublishDropdown({
const isPublished = currentFlow?.access_type === "PUBLIC";
const hasIO = useFlowStore((state) => state.hasIO);
const isAuth = useAuthStore((state) => !!state.autoLogin);
const { can } = usePermissions();
// Publishing changes the flow's access settings → gate on write. Only the
// publish controls are gated; the rest of the menu (API access, export,
// MCP, embed) stays available to read-only users.
const canShare = can(flowId, "write");
const [openExportModal, setOpenExportModal] = useState(false);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const { t } = useTranslation();

Expand Down Expand Up @@ -158,7 +164,7 @@ export default function PublishDropdown({
{ENABLE_PUBLISH && (
<DropdownMenuItem
className="deploy-dropdown-item group"
disabled={!hasIO}
disabled={!canShare || !hasIO}
onClick={() => {}}
data-testid="shareable-playground"
>
Expand Down Expand Up @@ -204,7 +210,7 @@ export default function PublishDropdown({
data-testid="publish-switch"
className="scale-[85%]"
checked={isPublished}
disabled={!hasIO}
disabled={!canShare || !hasIO}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { PermissionsProvider } from "@/contexts/permissionsContext";
import useFlowStore from "@/stores/flowStore";
import useFlowsManagerStore from "@/stores/flowsManagerStore";
import DeployButton from "./deploy-button";
import PublishDropdown from "./deploy-dropdown";
import PlaygroundButton from "./playground-button";
Expand All @@ -12,16 +14,30 @@ const FlowToolbarOptions = ({
setOpenApiModal,
}: FlowToolbarOptionsProps) => {
const hasIO = useFlowStore((state) => state.hasIO);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
// Scope to the flow's project so the toolbar evaluates the same
// domain-scoped permission set as the project list (HomePage).
const currentFlowFolderId = useFlowsManagerStore(
(state) => state.currentFlow?.folder_id,
);

return (
<div className="flex items-center gap-1">
<PlaygroundButton hasIO={hasIO} />
<PublishDropdown
openApiModal={openApiModal}
setOpenApiModal={setOpenApiModal}
/>
<DeployButton />
</div>
<PermissionsProvider
resourceType="flow"
resourceIds={currentFlowId ? [currentFlowId] : []}
domain={
currentFlowFolderId ? `project:${currentFlowFolderId}` : undefined
}
>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<div className="flex items-center gap-1">
<PlaygroundButton hasIO={hasIO} />
<PublishDropdown
openApiModal={openApiModal}
setOpenApiModal={setOpenApiModal}
/>
<DeployButton />
</div>
</PermissionsProvider>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { useTranslation } from "react-i18next";
import ForwardedIconComponent from "@/components/common/genericIconComponent";
import ShadTooltip from "@/components/common/shadTooltipComponent";
import { SimpleSidebarTrigger } from "@/components/ui/simple-sidebar";
import { usePermissions } from "@/contexts/permissionsContext";
import useFlowsManagerStore from "@/stores/flowsManagerStore";

interface PlaygroundButtonProps {
hasIO: boolean;
Expand All @@ -24,6 +26,21 @@ const DisabledButton = () => (

const PlaygroundButton = ({ hasIO }: PlaygroundButtonProps) => {
const { t } = useTranslation();
const { can } = usePermissions();
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
// Running a flow in the playground executes it → gate on the execute action.
const canRun = can(currentFlowId, "execute");

if (!canRun) {
return (
<ShadTooltip content={t("misc.playground")}>
<div>
<DisabledButton />
</div>
</ShadTooltip>
);
}

return hasIO ? (
<SimpleSidebarTrigger>
<ButtonLabel />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SelectItem,
SelectTrigger,
} from "@/components/ui/select-custom";
import { usePermissions } from "@/contexts/permissionsContext";
import type { FolderType } from "@/pages/MainPage/entities";
import { cn } from "@/utils/utils";
import { handleSelectChange } from "../helpers/handle-select-change";
Expand All @@ -27,6 +28,10 @@ export const SelectOptions = ({
checkPathName: (folderId: string) => boolean;
}) => {
const { t } = useTranslation();
const { can } = usePermissions();
const canRename = can(item.id, "write");
const canDownload = can(item.id, "read");
const canDelete = can(item.id, "delete");
return (
<div>
<Select
Expand Down Expand Up @@ -68,20 +73,23 @@ export const SelectOptions = ({
value="rename"
data-testid="btn-rename-project"
className="text-xs"
disabled={!canRename}
>
<FolderSelectItem name={t("folder.rename")} iconName="SquarePen" />
</SelectItem>
<SelectItem
value="download"
data-testid="btn-download-project"
className="text-xs"
disabled={!canDownload}
>
<FolderSelectItem name={t("folder.download")} iconName="Download" />
</SelectItem>
<SelectItem
value="delete"
data-testid="btn-delete-project"
className="text-xs"
disabled={!canDelete}
>
<FolderSelectItem name={t("folder.delete")} iconName="Trash2" />
</SelectItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from "@/components/ui/sidebar";
import { PermissionsProvider } from "@/contexts/permissionsContext";
import { useUpdateUser } from "@/controllers/API/queries/auth";
import {
usePatchFolders,
Expand Down Expand Up @@ -399,96 +400,107 @@ const SideBarFoldersButtonsComponent = ({
<SidebarContent>
<SidebarGroup className="p-4 py-2">
<SidebarGroupContent>
<SidebarMenu>
{!loading ? (
folders.length === 0 ? (
<div className="px-2 py-5 text-center text-sm text-muted-foreground">
{t("sidebar.emptyMessage")}
</div>
) : (
folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
)[0];
return (
<SidebarMenuItem
key={index}
className="group/menu-button"
onMouseEnter={() => setHoveredFolderId(item.id!)}
onMouseLeave={() => setHoveredFolderId(null)}
>
<div className="relative flex w-full">
<SidebarMenuButton
size="md"
onDragOver={(e) => dragOver(e, item.id!)}
onDragEnter={(e) => dragEnter(e, item.id!)}
onDragLeave={dragLeave}
onDrop={(e) => onDrop(e, item.id!)}
key={item.id}
data-testid={`sidebar-nav-${item.name}`}
id={`sidebar-nav-${item.name}`}
isActive={checkPathName(item.id!)}
onClick={() => handleChangeFolder!(item.id!)}
className={cn(
"flex-grow pr-8",
hoveredFolderId === item.id && "bg-accent",
checkHoveringFolder(item.id!),
)}
>
<div
onDoubleClick={(event) => {
handleDoubleClick(event, item);
}}
className="flex w-full items-center justify-between gap-2"
<PermissionsProvider
resourceType="project"
resourceIds={folders
.map((folder) => folder.id ?? "")
.filter(Boolean)}
>
<SidebarMenu>
{!loading ? (
folders.length === 0 ? (
<div className="px-2 py-5 text-center text-sm text-muted-foreground">
{t("sidebar.emptyMessage")}
</div>
) : (
folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
)[0];
return (
<SidebarMenuItem
key={index}
className="group/menu-button"
onMouseEnter={() => setHoveredFolderId(item.id!)}
onMouseLeave={() => setHoveredFolderId(null)}
>
<div className="relative flex w-full">
<SidebarMenuButton
size="md"
onDragOver={(e) => dragOver(e, item.id!)}
onDragEnter={(e) => dragEnter(e, item.id!)}
onDragLeave={dragLeave}
onDrop={(e) => onDrop(e, item.id!)}
key={item.id}
data-testid={`sidebar-nav-${item.name}`}
id={`sidebar-nav-${item.name}`}
isActive={checkPathName(item.id!)}
onClick={() => handleChangeFolder!(item.id!)}
className={cn(
"flex-grow pr-8",
hoveredFolderId === item.id && "bg-accent",
checkHoveringFolder(item.id!),
)}
>
<div className="flex flex-1 items-center gap-2">
{editFolderName?.edit && !isUpdatingFolder ? (
<InputEditFolderName
handleEditFolderName={handleEditFolderName}
item={item}
refInput={refInput}
handleKeyDownFn={handleKeyDownFn}
handleEditNameFolder={handleEditNameFolder}
editFolderName={editFolderName}
foldersNames={foldersNames}
handleKeyDown={handleKeyDown}
/>
) : (
<span className="block w-0 grow truncate text-sm opacity-100">
{item.name}
</span>
)}
<div
onDoubleClick={(event) => {
handleDoubleClick(event, item);
}}
className="flex w-full items-center justify-between gap-2"
>
<div className="flex flex-1 items-center gap-2">
{editFolderName?.edit && !isUpdatingFolder ? (
<InputEditFolderName
handleEditFolderName={
handleEditFolderName
}
item={item}
refInput={refInput}
handleKeyDownFn={handleKeyDownFn}
handleEditNameFolder={
handleEditNameFolder
}
editFolderName={editFolderName}
foldersNames={foldersNames}
handleKeyDown={handleKeyDown}
/>
) : (
<span className="block w-0 grow truncate text-sm opacity-100">
{item.name}
</span>
)}
</div>
</div>
</SidebarMenuButton>
<div
className="absolute right-2 top-[0.45rem] flex items-center hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<SelectOptions
item={item}
handleDeleteFolder={handleDeleteFolder}
handleDownloadFolder={() =>
handleDownloadFolder(item.id!, item.name)
}
handleSelectFolderToRename={
handleSelectFolderToRename
}
checkPathName={checkPathName}
/>
</div>
</SidebarMenuButton>
<div
className="absolute right-2 top-[0.45rem] flex items-center hover:text-foreground"
onClick={(e) => e.stopPropagation()}
>
<SelectOptions
item={item}
handleDeleteFolder={handleDeleteFolder}
handleDownloadFolder={() =>
handleDownloadFolder(item.id!, item.name)
}
handleSelectFolderToRename={
handleSelectFolderToRename
}
checkPathName={checkPathName}
/>
</div>
</div>
</SidebarMenuItem>
);
})
)
) : (
<>
<SidebarFolderSkeleton />
<SidebarFolderSkeleton />
</>
)}
</SidebarMenu>
</SidebarMenuItem>
);
})
)
) : (
<>
<SidebarFolderSkeleton />
<SidebarFolderSkeleton />
</>
)}
</SidebarMenu>
</PermissionsProvider>
</SidebarGroupContent>
</SidebarGroup>
<div className="flex-1" />
Expand Down
Loading
Loading