[PAI-965] feat: ai cycle editability (#4746)
This commit is contained in:
@@ -226,15 +226,6 @@ async def prepare_execution_data(request: ActionBatchExecutionRequest, user_id:
|
||||
# No project_id found in original artifact - this might be okay for some entities
|
||||
log.warning(f"No project_id found in original artifact {artifact_item.artifact_id} (entity: {entity_type})")
|
||||
|
||||
# Resolve project_id to full project object for UI display
|
||||
try:
|
||||
from pi.services.actions.artifacts.utils import resolve_project_id_to_object
|
||||
|
||||
tool_args = await resolve_project_id_to_object(tool_args)
|
||||
log.info(f"Resolved project data for artifact {artifact_item.artifact_id}")
|
||||
except Exception as e:
|
||||
log.error(f"Error resolving project_id for artifact {artifact_item.artifact_id}: {e}")
|
||||
|
||||
# Create ActionArtifactVersion and use its UUID as step_id
|
||||
try:
|
||||
from pi.services.actions.artifacts.utils import convert_uuids_to_strings
|
||||
|
||||
@@ -573,9 +573,9 @@ async def prepare_edited_artifact_data(entity_type: str, artifact_data: dict) ->
|
||||
edited_preparation_functions = {
|
||||
"workitem": prepare_edited_workitem_artifact_data,
|
||||
"epic": prepare_edited_workitem_artifact_data,
|
||||
"cycle": prepare_edited_cycle_artifact_data,
|
||||
# TODO: Add other entity types later
|
||||
# "project": prepare_edited_project_artifact_data,
|
||||
# "cycle": prepare_edited_cycle_artifact_data,
|
||||
# "module": prepare_edited_module_artifact_data,
|
||||
# "comment": prepare_edited_comment_artifact_data,
|
||||
# "state": prepare_edited_state_artifact_data,
|
||||
@@ -588,6 +588,62 @@ async def prepare_edited_artifact_data(entity_type: str, artifact_data: dict) ->
|
||||
return prepared_data
|
||||
|
||||
|
||||
async def prepare_edited_cycle_artifact_data(artifact_data: dict) -> dict:
|
||||
"""Prepare edited cycle artifact data for execution."""
|
||||
clean_data: dict = {}
|
||||
properties: dict[str, Any] = {}
|
||||
|
||||
# Essential top-level fields for cycles (from CYCLE_FIELDS schema)
|
||||
# Note: start_date and end_date should be at top level for tool execution
|
||||
essential_fields = {"name", "description", "project", "project_id", "start_date", "end_date"}
|
||||
|
||||
# Extract and preserve entity_info first (for executed artifacts)
|
||||
entity_info = None
|
||||
if isinstance(artifact_data, dict) and "entity_info" in artifact_data:
|
||||
entity_info = artifact_data.get("entity_info")
|
||||
log.info(f"Found entity_info in edited cycle artifact data: {entity_info}")
|
||||
|
||||
for key, value in artifact_data.items():
|
||||
# Skip null/empty values
|
||||
if value is None or (isinstance(value, list) and not value):
|
||||
continue
|
||||
|
||||
if key in essential_fields:
|
||||
# For dates, ensure they're strings (not objects)
|
||||
if key in ["start_date", "end_date"] and isinstance(value, dict):
|
||||
# Extract the date string from object format {"name": "2025-11-11"}
|
||||
clean_data[key] = str(value.get("name", value))
|
||||
else:
|
||||
clean_data[key] = value
|
||||
elif key == "owned_by" and value:
|
||||
# Handle owned_by field (user who owns the cycle)
|
||||
if isinstance(value, dict):
|
||||
properties["owned_by"] = value
|
||||
else:
|
||||
# If it's just an ID, keep it as string
|
||||
properties["owned_by"] = str(value)
|
||||
elif key not in {"entity_info"}: # Skip entity_info
|
||||
properties[key] = value
|
||||
|
||||
# Convert project object to project_id string (frontend sends project as object)
|
||||
if "project" in clean_data and isinstance(clean_data["project"], dict):
|
||||
project_obj = clean_data.pop("project")
|
||||
if "id" in project_obj:
|
||||
clean_data["project_id"] = str(project_obj["id"])
|
||||
log.info(f"Converted project object to project_id: {clean_data["project_id"]}")
|
||||
|
||||
# Add properties if any
|
||||
if properties:
|
||||
clean_data["properties"] = properties
|
||||
|
||||
# Restore entity_info if it was present
|
||||
if entity_info:
|
||||
clean_data["entity_info"] = entity_info
|
||||
log.info(f"Restored entity_info to clean cycle data: {entity_info}")
|
||||
|
||||
return clean_data
|
||||
|
||||
|
||||
async def prepare_edited_workitem_artifact_data(artifact_data: dict) -> dict:
|
||||
# Start with basic fields
|
||||
clean_data: dict = {}
|
||||
|
||||
@@ -453,8 +453,8 @@ class PlaneSDKAdapter:
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to get current user for cycle ownership: {e}")
|
||||
|
||||
# Prepare cycle data
|
||||
cycle_data = {"name": name, **kwargs}
|
||||
# log.debug(f"cycle_data payload prepared: {cycle_data}")
|
||||
if start_date:
|
||||
cycle_data["start_date"] = datetime.fromisoformat(start_date.replace("Z", "+00:00"))
|
||||
if end_date:
|
||||
@@ -464,13 +464,21 @@ class PlaneSDKAdapter:
|
||||
if owned_by:
|
||||
cycle_data["owned_by"] = owned_by
|
||||
|
||||
# Create the request object
|
||||
cycle_request = CycleCreateRequest(**cycle_data)
|
||||
# Log the serialized request to see exactly what fields are included
|
||||
try:
|
||||
cycle_request.model_dump() if hasattr(cycle_request, "model_dump") else cycle_request.dict()
|
||||
# log.debug(f"CycleCreateRequest model dump: {dump}")
|
||||
except Exception as dump_error:
|
||||
log.warning(f"Failed to dump CycleCreateRequest model: {dump_error}")
|
||||
|
||||
# WORKAROUND: Backend API requires project_id in body when dates are present.
|
||||
# We monkey-patch model_dump() to inject project_id when needed.
|
||||
if (start_date or end_date) and project_id:
|
||||
original_model_dump = cycle_request.model_dump
|
||||
|
||||
def model_dump_with_project_id(*args, **kwargs):
|
||||
result = original_model_dump(*args, **kwargs)
|
||||
result["project_id"] = project_id
|
||||
return result
|
||||
|
||||
# Use object.__setattr__ to bypass Pydantic's attribute protection
|
||||
object.__setattr__(cycle_request, "model_dump", model_dump_with_project_id)
|
||||
|
||||
# Call SDK method
|
||||
cycle = self.cycles.create_cycle(project_id=project_id, slug=workspace_slug, cycle_create_request=cycle_request)
|
||||
|
||||
@@ -16,14 +16,16 @@ import { ProjectDropdown } from "@/components/dropdowns/project/dropdown";
|
||||
import { useUser } from "@/hooks/store/user/user-user";
|
||||
|
||||
type Props = {
|
||||
handleFormSubmit: (values: Partial<ICycle>) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
status: boolean;
|
||||
projectId: string;
|
||||
setActiveProject: (projectId: string) => void;
|
||||
data?: ICycle | null;
|
||||
data?: Partial<ICycle> | null;
|
||||
isMobile?: boolean;
|
||||
isBackwardDateEditEnabled?: boolean;
|
||||
onChange?: (formData: Partial<ICycle> | null) => Promise<void>;
|
||||
showActionButtons?: boolean;
|
||||
handleClose?: () => void;
|
||||
handleFormSubmit?: (values: Partial<ICycle>) => Promise<void>;
|
||||
};
|
||||
|
||||
const defaultValues: Partial<ICycle> = {
|
||||
@@ -43,6 +45,8 @@ export function CycleForm(props: Props) {
|
||||
data,
|
||||
isMobile = false,
|
||||
isBackwardDateEditEnabled = false,
|
||||
showActionButtons = true,
|
||||
onChange,
|
||||
} = props;
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
@@ -50,9 +54,10 @@ export function CycleForm(props: Props) {
|
||||
const { projectsWithCreatePermissions } = useUser();
|
||||
// form data
|
||||
const {
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isDirty, isSubmitting, dirtyFields },
|
||||
handleSubmit,
|
||||
control,
|
||||
watch,
|
||||
reset,
|
||||
} = useForm<ICycle>({
|
||||
defaultValues: {
|
||||
@@ -63,6 +68,10 @@ export function CycleForm(props: Props) {
|
||||
end_date: data?.end_date || null,
|
||||
},
|
||||
});
|
||||
const handleOnChange = () => {
|
||||
if (!onChange) return;
|
||||
onChange(watch());
|
||||
};
|
||||
|
||||
const { getIndex } = getTabIndex(ETabIndices.PROJECT_CYCLE, isMobile);
|
||||
|
||||
@@ -74,7 +83,7 @@ export function CycleForm(props: Props) {
|
||||
}, [data, reset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit((formData) => handleFormSubmit(formData))}>
|
||||
<form onSubmit={handleSubmit((formData) => handleFormSubmit?.(formData))}>
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex items-center gap-x-3">
|
||||
{!status && (
|
||||
@@ -89,6 +98,7 @@ export function CycleForm(props: Props) {
|
||||
if (!Array.isArray(val)) {
|
||||
onChange(val);
|
||||
setActiveProject(val);
|
||||
handleOnChange();
|
||||
}
|
||||
}}
|
||||
multiple={false}
|
||||
@@ -124,7 +134,10 @@ export function CycleForm(props: Props) {
|
||||
className="w-full text-base"
|
||||
value={value}
|
||||
inputSize="md"
|
||||
onChange={onChange}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
handleOnChange();
|
||||
}}
|
||||
hasError={Boolean(errors?.name)}
|
||||
tabIndex={getIndex("description")}
|
||||
autoFocus
|
||||
@@ -144,7 +157,10 @@ export function CycleForm(props: Props) {
|
||||
className="w-full text-base resize-none min-h-24"
|
||||
hasError={Boolean(errors?.description)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
handleOnChange();
|
||||
}}
|
||||
tabIndex={getIndex("description")}
|
||||
/>
|
||||
)}
|
||||
@@ -170,6 +186,7 @@ export function CycleForm(props: Props) {
|
||||
onSelect={(val) => {
|
||||
onChangeStartDate(val?.from ? renderFormattedPayloadDate(val.from) : null);
|
||||
onChangeEndDate(val?.to ? renderFormattedPayloadDate(val.to) : null);
|
||||
handleOnChange();
|
||||
}}
|
||||
placeholder={{
|
||||
from: "Start date",
|
||||
@@ -187,20 +204,22 @@ export function CycleForm(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? t("common.updating")
|
||||
: t("project_cycles.update_cycle")
|
||||
: isSubmitting
|
||||
? t("common.creating")
|
||||
: t("project_cycles.create_cycle")}
|
||||
</Button>
|
||||
</div>
|
||||
{showActionButtons && (
|
||||
<div className="px-5 py-4 flex items-center justify-end gap-2 border-t-[0.5px] border-custom-border-200">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={getIndex("cancel")}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button variant="primary" size="sm" type="submit" loading={isSubmitting} tabIndex={getIndex("submit")}>
|
||||
{data
|
||||
? isSubmitting
|
||||
? t("common.updating")
|
||||
: t("project_cycles.update_cycle")
|
||||
: isSubmitting
|
||||
? t("common.creating")
|
||||
: t("project_cycles.create_cycle")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ export const DisplayDates = (props: {
|
||||
}) => {
|
||||
const { startDate, endDate, className } = props;
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1 text-custom-text-300", className)}>
|
||||
<CalendarLayoutIcon className="size-3 flex-shrink-0" />
|
||||
<div className="text-sm"> {renderFormattedDate(startDate)}</div>
|
||||
<div className={cn("flex items-center gap-1 text-custom-text-300 flex-wrap", className)}>
|
||||
<CalendarLayoutIcon className="size-4 flex-shrink-0" />
|
||||
<div className="text-sm flex-shrink-0"> {renderFormattedDate(startDate)}</div>
|
||||
{startDate && endDate && <ArrowRight className="h-3 w-3 flex-shrink-0" />}
|
||||
<div className="text-sm"> {renderFormattedDate(endDate)}</div>
|
||||
<div className="text-sm flex-shrink-0"> {renderFormattedDate(endDate)}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import type { ICycle } from "@plane/types";
|
||||
import { Card } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
import { CycleForm } from "@/components/cycles/form";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
import type { TUpdatedArtifact, TArtifact } from "@/plane-web/types";
|
||||
import { useCycleData } from "../useArtifactData";
|
||||
import { PiChatArtifactsFooter } from "./footer";
|
||||
|
||||
interface TCycleDetailProps {
|
||||
data: TArtifact;
|
||||
updateArtifact: (data: TUpdatedArtifact) => Promise<void>;
|
||||
workspaceSlug: string;
|
||||
activeChatId: string;
|
||||
}
|
||||
|
||||
export const CycleDetail = observer((props: TCycleDetailProps) => {
|
||||
const { data, updateArtifact, workspaceSlug, activeChatId } = props;
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const updatedData = useCycleData(data.artifact_id);
|
||||
// state
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showSavedToast, setShowSavedToast] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// derived values
|
||||
const projectId = data.parameters?.project?.id;
|
||||
// handlers
|
||||
const handleOnSave = () => {
|
||||
setIsSaving(false);
|
||||
setShowSavedToast(true);
|
||||
setTimeout(() => {
|
||||
setShowSavedToast(false);
|
||||
setError(null);
|
||||
}, 1000);
|
||||
};
|
||||
const onChange = async (formData: Partial<ICycle> | null) => {
|
||||
if (!formData) return;
|
||||
setIsSaving(true);
|
||||
await updateArtifact(formData)
|
||||
.then(() => {
|
||||
handleOnSave();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
setError(error);
|
||||
handleOnSave();
|
||||
});
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Card className="relative max-w-[700px] rounded-xl shadow-lg p-0 space-y-0">
|
||||
<CycleForm
|
||||
onChange={onChange}
|
||||
status
|
||||
projectId={projectId ?? ""}
|
||||
setActiveProject={() => {}}
|
||||
data={updatedData}
|
||||
isMobile={isMobile}
|
||||
showActionButtons={false}
|
||||
/>
|
||||
<div
|
||||
className={cn("absolute top-0 right-0 w-full h-full bg-custom-background-100 rounded-xl opacity-50", {
|
||||
hidden: data.is_editable,
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
<PiChatArtifactsFooter
|
||||
artifactsData={data}
|
||||
workspaceSlug={workspaceSlug}
|
||||
activeChatId={activeChatId}
|
||||
artifactId={data.artifact_id}
|
||||
isSaving={isSaving}
|
||||
showSavedToast={showSavedToast}
|
||||
error={error}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -21,10 +21,10 @@ export const ModuleDetail = observer((props: TModuleDetailProps) => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const updatedData = useModuleData(data.artifact_id);
|
||||
// state
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showSavedToast, setShowSavedToast] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// state
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showSavedToast, setShowSavedToast] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// derived values
|
||||
const projectId = data.parameters?.project?.id;
|
||||
// handlers
|
||||
@@ -51,30 +51,31 @@ export const ModuleDetail = observer((props: TModuleDetailProps) => {
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Card className="relative max-w-[700px] rounded-xl shadow-lg p-0 space-y-0">
|
||||
<ModuleForm
|
||||
onChange={onChange}
|
||||
status
|
||||
projectId={projectId ?? ""}
|
||||
setActiveProject={() => {}}
|
||||
data={updatedData}
|
||||
isMobile={isMobile}
|
||||
showActionButtons={false}
|
||||
<Card className="relative max-w-[700px] rounded-xl shadow-lg p-0 space-y-0">
|
||||
<ModuleForm
|
||||
onChange={onChange}
|
||||
status
|
||||
projectId={projectId ?? ""}
|
||||
setActiveProject={() => {}}
|
||||
data={updatedData}
|
||||
isMobile={isMobile}
|
||||
showActionButtons={false}
|
||||
/>
|
||||
<div
|
||||
className={cn("absolute top-0 right-0 w-full h-full bg-custom-background-100 rounded-xl opacity-50", {
|
||||
hidden: data.is_editable,
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
<PiChatArtifactsFooter
|
||||
artifactsData={data}
|
||||
workspaceSlug={workspaceSlug}
|
||||
activeChatId={activeChatId}
|
||||
artifactId={data.artifact_id}
|
||||
isSaving={isSaving}
|
||||
showSavedToast={showSavedToast}
|
||||
error={error}
|
||||
/>
|
||||
<div
|
||||
className={cn("absolute top-0 right-0 w-full h-full bg-custom-background-100 rounded-xl opacity-50", {
|
||||
hidden: data.is_editable,
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
<PiChatArtifactsFooter
|
||||
artifactsData={data}
|
||||
workspaceSlug={workspaceSlug}
|
||||
activeChatId={activeChatId}
|
||||
artifactId={data.artifact_id}
|
||||
isSaving={isSaving}
|
||||
showSavedToast={showSavedToast}
|
||||
error={error}
|
||||
/></>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from "next/navigation";
|
||||
import { cn } from "@plane/utils";
|
||||
import { usePiChat } from "@/plane-web/hooks/store/use-pi-chat";
|
||||
import type { TArtifact, TUpdatedArtifact } from "@/plane-web/types";
|
||||
import { CycleDetail } from "./cycle";
|
||||
import { EpicDetail } from "./epic";
|
||||
import { Header } from "./header";
|
||||
import { ModuleDetail } from "./module";
|
||||
@@ -27,7 +28,9 @@ const DetailCardRenderer = observer(
|
||||
case "epic":
|
||||
return <EpicDetail {...props} />;
|
||||
case "module":
|
||||
return <ModuleDetail {...props} />;
|
||||
return <ModuleDetail {...props} />;
|
||||
case "cycle":
|
||||
return <CycleDetail {...props} />;
|
||||
default:
|
||||
return <TemplateDetail {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { CycleIcon } from "@plane/propel/icons";
|
||||
import { useTemplateData } from "../useArtifactData";
|
||||
import { Properties } from "./properties";
|
||||
import { cn } from "@plane/utils";
|
||||
import { DisplayDates } from "@/components/properties/dates";
|
||||
import { useCycleData } from "../useArtifactData";
|
||||
import { WithPreviewHOC } from "./with-preview-hoc";
|
||||
|
||||
type TProps = {
|
||||
@@ -10,16 +11,24 @@ type TProps = {
|
||||
|
||||
export const CyclePreviewCard = observer((props: TProps) => {
|
||||
const { artifactId } = props;
|
||||
const data = useTemplateData(artifactId);
|
||||
const properties = { ...data?.parameters?.properties, project: data?.parameters?.project };
|
||||
const data = useCycleData(artifactId);
|
||||
if (!data) return <></>;
|
||||
return (
|
||||
<WithPreviewHOC artifactId={data.artifact_id}>
|
||||
<WithPreviewHOC artifactId={artifactId}>
|
||||
<div className="flex gap-2 items-start">
|
||||
<CycleIcon className="size-4 text-custom-text-100 my-0.5" />
|
||||
<div className="flex flex-col">
|
||||
<div className="truncate text-sm font-medium text-start capitalize">{data.parameters?.name || "Unknown"}</div>
|
||||
{properties && <Properties {...properties} />}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="truncate text-sm font-medium text-start capitalize">{data?.name || "Unknown"}</div>
|
||||
{/* properties */}
|
||||
{(data.start_date || data.end_date) && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-wrap gap-2 items-center [&>*]:p-0 [&>*]:hover:bg-transparent text-sm text-custom-text-300"
|
||||
)}
|
||||
>
|
||||
<DisplayDates startDate={data.start_date ?? null} endDate={data.end_date ?? null} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</WithPreviewHOC>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isEmpty } from "lodash-es";
|
||||
import type { IModule, TIssue, TIssuePriorities, TPage } from "@plane/types";
|
||||
import type { ICycle, IModule, TIssue, TIssuePriorities, TPage } from "@plane/types";
|
||||
import { usePiChat } from "@/plane-web/hooks/store/use-pi-chat";
|
||||
import type { TArtifact, TUpdatedArtifact } from "@/plane-web/types";
|
||||
|
||||
@@ -81,10 +81,29 @@ export const useTemplateData = (artifactId: string): TArtifact | undefined => {
|
||||
return getArtifact(artifactId);
|
||||
};
|
||||
|
||||
export const useCycleData = (artifactId: string): Partial<ICycle> => {
|
||||
const {
|
||||
artifactsStore: { getArtifact, getArtifactByVersion },
|
||||
} = usePiChat();
|
||||
|
||||
const originalData = getArtifact(artifactId);
|
||||
const updatedData = getArtifactByVersion(artifactId, "updated");
|
||||
const parameters = originalData?.parameters;
|
||||
return !isEmpty(updatedData as Partial<ICycle>)
|
||||
? (updatedData as Partial<ICycle>)
|
||||
: {
|
||||
name: parameters?.name,
|
||||
description: parameters?.description || "",
|
||||
start_date: parameters?.properties?.start_date?.name || null,
|
||||
end_date: parameters?.properties?.end_date?.name || null,
|
||||
};
|
||||
};
|
||||
|
||||
export const useArtifactData = (artifactId: string, artifactType?: string): TUpdatedArtifact => {
|
||||
const issueData = useWorkItemData(artifactId);
|
||||
const templateData = useTemplateData(artifactId);
|
||||
const pageData = usePageData(artifactId);
|
||||
const cycleData = useCycleData(artifactId);
|
||||
const moduleData = useModuleData(artifactId);
|
||||
switch (artifactType) {
|
||||
case "workitem":
|
||||
@@ -93,6 +112,8 @@ export const useArtifactData = (artifactId: string, artifactType?: string): TUpd
|
||||
return pageData;
|
||||
case "epic":
|
||||
return issueData;
|
||||
case "cycle":
|
||||
return cycleData;
|
||||
case "module":
|
||||
return moduleData;
|
||||
default:
|
||||
|
||||
Vendored
+1
-1
@@ -263,4 +263,4 @@ export type TInstanceResponse =
|
||||
};
|
||||
|
||||
// constants
|
||||
export const EDITABLE_ARTIFACT_TYPES = ["workitem", "epic", "page", "module"];
|
||||
export const EDITABLE_ARTIFACT_TYPES = ["workitem", "epic", "page", "cycle", "module"];
|
||||
|
||||
Reference in New Issue
Block a user