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
78 changes: 76 additions & 2 deletions packages/open-workflow-diagram-editor/src/core/taskDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,39 @@ const MAX_DEPTH = 4;

/* Flattened task row - kind: how the view should render it */
export type DetailField =
| { path: string; kind: "text"; display: string }
| { path: string; kind: "scalar"; value: string | number | boolean }
| { path: string; kind: "enum"; value: string; options: string[] }
| { path: string; kind: "runtime-expression"; value: string }
| { path: string; kind: "duration"; value: string }
| { path: string; kind: "long-string"; value: string }
| { path: string; kind: "array"; count: number }
| { path: string; kind: "object" };

function isPlainObject(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}

// This is temporary function until we dynamically build the form with field types
function isLongStringField(path: string): boolean {
Comment thread
kumaradityaraj marked this conversation as resolved.
return path === "run.shell.command" || path === "run.script.code";
}

function isRuntimeExpressionField(path: string): boolean {
return path === "if";
}

function isDurationField(path: string): boolean {
return path === "timeout" || path === "timeout.after";
}

const ENUM_FIELDS: Record<string, string[]> = {
"with.output": ["raw", "content", "response"],
};

function getEnumOptions(path: string): string[] | undefined {
return ENUM_FIELDS[path];
}

function flattenFields(
value: unknown,
path: string = "",
Expand All @@ -57,9 +82,58 @@ function flattenFields(
for (const [key, val] of Object.entries(value)) {
flattenFields(val, path ? `${path}.${key}` : key, depth + 1, outputFields);
}

return;
}

if (typeof value === "string" && isLongStringField(path)) {
outputFields.push({
path,
kind: "long-string",
value,
});
return;
}
outputFields.push({ path, kind: "text", display: String(value) });

if (typeof value === "string" && isRuntimeExpressionField(path)) {
outputFields.push({
path,
kind: "runtime-expression",
value,
});
return;
}

if (typeof value === "string" && isDurationField(path)) {
outputFields.push({
path,
kind: "duration",
value,
});
return;
}

if (typeof value === "string") {
const options = getEnumOptions(path);

if (options) {
outputFields.push({
path,
kind: "enum",
value,
options,
});
return;
}
}

if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
outputFields.push({
path,
kind: "scalar",
value,
});
}
}

/* Builds the flattened detail rows for a task: task-specific fields first, inherited base fields last */
Expand Down
3 changes: 3 additions & 0 deletions packages/open-workflow-diagram-editor/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export const en = {
"toast.clipboard.error": "Failed to copy",
"toast.download.success": "Download started",
"toast.download.error": "Download failed",
"sidebar.duration.title": "Enter an ISO 8601 duration, for example PT30S or PT5M",
"sidebar.field.item": "item",
"sidebar.field.items": "items",
} as const;

export type TranslationKeys = keyof typeof en;
135 changes: 135 additions & 0 deletions packages/open-workflow-diagram-editor/src/side-panel/FieldControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Copyright 2021-Present The Open Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { DetailField } from "@/core/taskDetails";
import {
Combobox,
ComboboxContent,
ComboboxItem,
ComboboxList,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { useI18n } from "@openworkflowspec/i18n";

type ControlProps<K extends DetailField["kind"]> = {
field: Extract<DetailField, { kind: K }>;
isReadOnly: boolean;
};

const ISO_8601_DURATION_REGEX =
/^P(?=\d|T)(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?=\d)(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/;

function LongStringControl({ field, isReadOnly }: ControlProps<"long-string">) {
return <Textarea value={field.value} readOnly disabled={isReadOnly} />;
}

function DurationControl({ field, isReadOnly }: ControlProps<"duration">) {
const { t } = useI18n();
return (
<Input
value={field.value}
disabled={isReadOnly}
pattern={ISO_8601_DURATION_REGEX.source}
title={t("sidebar.duration.title")}
/>
);
}

function ExpressionControl({ field, isReadOnly }: ControlProps<"runtime-expression">) {
return (
<div>
<span className="dec-sidebar-hint-text">Runtime expression</span>
<Input value={field.value} disabled={isReadOnly} />
</div>
);
}

function EnumControl({ field, isReadOnly }: ControlProps<"enum">) {
return (
<Combobox value={field.value} disabled={isReadOnly}>
<ComboboxTrigger>
<ComboboxValue placeholder="Select an option" />
</ComboboxTrigger>
<ComboboxContent>
<ComboboxList>
{field.options.map((option) => (
<ComboboxItem key={option} value={option}>
{option}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
}

function TextControl({ value, isReadOnly }: { value: string; isReadOnly: boolean }) {
return <Input value={value} disabled={isReadOnly} />;
}

function NumberControl({ value, isReadOnly }: { value: number; isReadOnly: boolean }) {
return <Input type="number" value={value} disabled={isReadOnly} />;
}

function BooleanControl({ value, isReadOnly }: { value: boolean; isReadOnly: boolean }) {
return <Switch checked={value} disabled={isReadOnly} />;
}

export function FieldControl({ field, isReadOnly }: { field: DetailField; isReadOnly: boolean }) {
const props = { isReadOnly };
const { t } = useI18n();

switch (field.kind) {
case "long-string":
return <LongStringControl field={field} {...props} />;

case "duration":
return <DurationControl field={field} {...props} />;

case "runtime-expression":
return <ExpressionControl field={field} {...props} />;

case "enum":
return <EnumControl field={field} {...props} />;

case "scalar":
if (typeof field.value === "string") {
return <TextControl value={field.value} {...props} />;
}

if (typeof field.value === "number") {
return <NumberControl value={field.value} {...props} />;
}

if (typeof field.value === "boolean") {
return <BooleanControl value={field.value} {...props} />;
}

return String(field.value);

case "array":
return `${field.count} ${t(
field.count === 1 ? "sidebar.field.item" : "sidebar.field.items",
)}`;

case "object":
return <>{"{...}"}</>;
}
}
17 changes: 15 additions & 2 deletions packages/open-workflow-diagram-editor/src/side-panel/Fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
* limitations under the License.
*/

import type { DetailField } from "@/core/taskDetails";
import { FieldControl } from "./FieldControls";

export function SectionHeader({ label }: { label: string }) {
return (
<div className="dec-sidebar-section-header">
Expand All @@ -32,11 +35,21 @@ export function InlineField({ label, value }: { label: string; value: string })
);
}

export function PropertyField({ label, value }: { label: string; value: string }) {
export function PropertyField({
label,
field,
isReadOnly,
}: {
label: string;
field: DetailField;
isReadOnly: boolean;
}) {
return (
<div className="dec-sidebar-prop">
<dt className="dec-sidebar-prop-label">{label}</dt>
<dd className="dec-sidebar-prop-value">{value}</dd>
<dd className="dec-sidebar-prop-value">
<FieldControl field={field} isReadOnly={isReadOnly} />
</dd>
</div>
);
Comment thread
kumaradityaraj marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,16 @@ type NodeDetailsViewProps = {
node: RF.Node<BaseNodeData>;
};

const OBJECT_GLYPH = "{...}";

function itemCount(length: number): string {
return `${length} item${length === 1 ? "" : "s"}`;
}

function fieldText(field: DetailField): string {
switch (field.kind) {
case "array":
return itemCount(field.count);
case "text":
return field.display;
case "object":
return OBJECT_GLYPH;
}
}

function FieldRow({ label, field }: { label: string; field: DetailField }) {
return <PropertyField label={label} value={fieldText(field)} />;
function FieldRow({
label,
field,
isReadOnly,
}: {
label: string;
field: DetailField;
isReadOnly: boolean;
}) {
return <PropertyField label={label} field={field} isReadOnly={isReadOnly} />;
}

export function NodeDetailsView({ node }: NodeDetailsViewProps) {
Expand Down Expand Up @@ -76,7 +67,7 @@ export function NodeDetailsView({ node }: NodeDetailsViewProps) {
<SectionHeader label={t("sidebar.sectionProperties")} />
<dl>
{fields.map((field) => (
<FieldRow key={field.path} label={field.path} field={field} />
<FieldRow key={field.path} label={field.path} field={field} isReadOnly={isReadOnly} />
))}
</dl>
</>
Expand Down
Loading