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
77 changes: 75 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,38 @@ 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);
}

function isLongStringField(path: string): boolean {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment to this section that it is temporary until we can dynamically build the form with field types

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 +81,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
99 changes: 97 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,24 @@
* limitations under the License.
*/

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

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)?)?$/;

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

export function PropertyField({ label, value }: { label: string; value: string }) {
function AutoGrowTextarea({ value, disabled }: { value: string; disabled: boolean }) {
const textareaRef = useRef<HTMLTextAreaElement>(null);

useEffect(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not fully sure why we need this? This looks like its overriding functionality in the shadcn component? Should just be able to use TextArea component directly? I can see it has the styles for height and size content?
https://github.com/open-workflow-specification/editor/blob/main/packages/open-workflow-diagram-editor/src/components/ui/textarea.tsx#L26

const textarea = textareaRef.current;

if (!textarea) {
return;
}

textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
}, [value]);

return <Textarea ref={textareaRef} value={value} disabled={disabled} />;
}

export function PropertyField({
label,
field,
isReadOnly,
}: {
label: string;
field: DetailField;
isReadOnly: boolean;
}) {
let control: ReactNode;

if (field.kind === "long-string") {
control = <AutoGrowTextarea value={field.value} disabled={isReadOnly} />;
} else if (field.kind === "runtime-expression") {
control = (
<div>
<span className="dec-sidebar-hint-text">Runtime expression</span>
<Input value={field.value} disabled={isReadOnly} />
</div>
);
} else if (field.kind === "duration") {
control = (
<Input
value={field.value}
disabled={isReadOnly}
pattern={ISO_8601_DURATION_REGEX.source}
title="Enter an ISO 8601 duration, for example PT30S or PT5M"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in a translation string

/>
);
} else if (field.kind === "enum") {
Comment thread
kumaradityaraj marked this conversation as resolved.
control = (
<Combobox value={field.value} disabled={isReadOnly}>
Comment thread
kumaradityaraj marked this conversation as resolved.
<ComboboxTrigger>
<ComboboxValue placeholder="Select an option" />
</ComboboxTrigger>

<ComboboxContent>
<ComboboxList>
{field.options.map((option) => (
<ComboboxItem key={option} value={option}>
{option}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
</Combobox>
);
} else if (field.kind === "scalar" && typeof field.value === "string") {
control = <Input value={field.value} disabled={isReadOnly} />;
} else if (field.kind === "scalar" && typeof field.value === "number") {
control = <Input type="number" value={field.value} disabled={isReadOnly} />;
} else if (field.kind === "scalar" && typeof field.value === "boolean") {
control = <Switch checked={field.value} disabled={isReadOnly} />;
} else if (field.kind === "scalar") {
control = String(field.value);
} else if (field.kind === "array") {
control = `${field.count} item${field.count === 1 ? "" : "s"}`;
} else {
control = "{...}";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think its cleaner to create a new file FieldControls.tsx that moves this logic. It would export a function that we can call here, something like

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

  switch (field.kind) {
    case "string":             return <TextControl field={field} {...props} />;
    case "number":             return <NumberControl field={field} {...props} />;
    case "boolean":            return <BooleanControl field={field} {...props} />;
    case "duration":           return <DurationControl field={field} {...props} />;
    case "runtime-expression": return <ExpressionControl field={field} {...props} />;
    case "code":               return <CodeControl field={field} {...props} />;
    case "enum":               return <EnumControl field={field} {...props} />;
    case "array":              return <>{t("sidebar.field.itemCount", { count: field.count })}</>;
    case "object":             return <>{"{...}"}</>;
  }
}

Above that export function define the fields explicitly then, for example

function TextControl({ field, isReadOnly }: ControlProps<"string">) {
  return <Input value={field.value} readOnly disabled={isReadOnly} />;
}

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

The update the PropertyFunction in this file to call that?
What do you think?

}

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">{control}</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