diff --git a/client/dive-common/apispec.ts b/client/dive-common/apispec.ts index d8888c6b1..f47f05cc6 100644 --- a/client/dive-common/apispec.ts +++ b/client/dive-common/apispec.ts @@ -42,6 +42,8 @@ interface DiveParam { type_props?: string[]; key: string; default: string; + /** True if the user must supply a value before the pipeline can run. */ + required?: boolean; } interface PipeMetadata { diff --git a/client/dive-common/components/PipelineParamsDialog.vue b/client/dive-common/components/PipelineParamsDialog.vue index 3585cbc03..f0c18566f 100644 --- a/client/dive-common/components/PipelineParamsDialog.vue +++ b/client/dive-common/components/PipelineParamsDialog.vue @@ -55,8 +55,14 @@ export default defineComponent({ strictlyPositive: (value: string | number) => Number(value) > 0 || 'Please enter a number > 0', }; - function getRules(type: PipelineParamType): ValidationRule[] { - const res = []; + function getRules(type: PipelineParamType, required = false): ValidationRule[] { + const res: ValidationRule[] = []; + if (required) { + res.push((value) => { + if (value === undefined || value === null || value === '') return 'Required'; + return true; + }); + } if (type.includes('int')) { res.push(rules.integer); } @@ -131,7 +137,7 @@ export default defineComponent({ :for="`input-${param.key}`" class="text-caption font-weight-bold text-uppercase text--secondary" > - {{ param.label }} + {{ param.label }} * @@ -157,7 +163,7 @@ export default defineComponent({ hide-details="auto" class="mt-1" :min="param.type === 'int' ? 'none' : 0" - :rules="getRules(param.type)" + :rules="getRules(param.type, param.required)" /> @@ -171,7 +177,7 @@ export default defineComponent({ hide-details="auto" class="mt-1" :min="param.type === 'float' ? 'none' : 0" - :rules="getRules(param.type)" + :rules="getRules(param.type, param.required)" /> @@ -193,7 +199,7 @@ export default defineComponent({ :min="param.type_props?.at(0) || 0" :max="param.type_props?.at(1) || 100" :step="param.type_props?.at(2) || 1" - :rules="getRules(param.type)" + :rules="getRules(param.type, param.required)" outlined hide-details /> @@ -211,6 +217,7 @@ export default defineComponent({ dense hide-details="auto" class="mt-1" + :rules="getRules(param.type, param.required)" /> diff --git a/client/platform/desktop/backend/native/common.ts b/client/platform/desktop/backend/native/common.ts index 3068bc910..ed5563ace 100644 --- a/client/platform/desktop/backend/native/common.ts +++ b/client/platform/desktop/backend/native/common.ts @@ -109,19 +109,38 @@ async function extractPipeMetadata(filePath: string): Promise { const [, label, rawArgs] = diveMatch; const args = rawArgs.split(',').map((arg) => arg.trim()); const type: PipelineParamType = args[0] as PipelineParamType; - const pipelineTypeArgs = args.slice(1); + const restArgs = args.slice(1); + // `required` is a flag keyword — strip it from type_props, + // everything else stays positional for the type. + const isRequired = restArgs.some((a) => a.toLowerCase() === 'required'); + const pipelineTypeArgs = restArgs.filter((a) => a.toLowerCase() !== 'required'); + + // `config = ` — absolute kwiver key, no process/block prefix + // applied. Used for global / cross-referenced settings. + const configMatch = trimmed.match(/^config\s+([\w:.-]+)\s*=\s*([^#]+)/i); + // Otherwise a regular per-process/block parameter assignment. + const paramLineMatch = !configMatch + ? trimmed.match(/^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)/i) + : null; + + let fullKey: string | null = null; + let defaultValue: string | null = null; + if (configMatch) { + fullKey = configMatch[1]; + defaultValue = configMatch[2].trim(); + } else if (paramLineMatch) { + fullKey = [...contextStack, paramLineMatch[1]].join(':'); + defaultValue = paramLineMatch[2].trim(); + } - const paramLineMatch = trimmed.match(/^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)/i); - if (paramLineMatch) { - const localKey = paramLineMatch[1]; - const defaultValue = paramLineMatch[2].trim(); - const fullKey = [...contextStack, localKey].join(':'); + if (fullKey !== null && defaultValue !== null) { metadata.diveParams!.push({ label, type, type_props: pipelineTypeArgs, key: fullKey, default: defaultValue, + ...(isRequired ? { required: true } : {}), }); } } diff --git a/server/dive_tasks/pipeline_discovery.py b/server/dive_tasks/pipeline_discovery.py index 426682b2b..889877ab4 100644 --- a/server/dive_tasks/pipeline_discovery.py +++ b/server/dive_tasks/pipeline_discovery.py @@ -72,22 +72,43 @@ def extract_pipe_metadata(file_path: Path) -> PipeMetadata: label, raw_args = dive_match.groups() args = [arg.strip() for arg in raw_args.split(',')] param_type = args[0] - pipeline_type_args = args[1:] + rest_args = args[1:] + # `required` is a flag keyword — strip it from type_props, + # everything else stays positional for the type. + is_required = any(a.lower() == 'required' for a in rest_args) + pipeline_type_args = [a for a in rest_args if a.lower() != 'required'] + + # `config = ` — absolute kwiver key, no + # process/block prefix. Used for global / cross-referenced + # settings. + config_match = re.match(r'^config\s+([\w:.-]+)\s*=\s*([^#]+)', trimmed, re.IGNORECASE) + # Otherwise a regular per-process/block parameter assignment. + param_line_match = ( + re.match(r'^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)', trimmed, re.IGNORECASE) + if not config_match else None + ) - param_line_match = re.match(r'^(?:relativepath\s+)?(?::)?([\w:-]+)\s*=?\s*([^#]+)', trimmed, - re.IGNORECASE) - if param_line_match: + full_key = None + default_val = None + if config_match: + full_key = config_match.group(1) + default_val = config_match.group(2).strip() + elif param_line_match: local_key = param_line_match.group(1) default_val = param_line_match.group(2).strip() full_key = ":".join(context_stack + [local_key]) - metadata["diveParams"].append({ + if full_key is not None and default_val is not None: + param_dict = { "label": label, "type": param_type, "type_props": pipeline_type_args, "key": full_key, - "default": default_val - }) + "default": default_val, + } + if is_required: + param_dict["required"] = True + metadata["diveParams"].append(param_dict) # --- Description extraction (Multiline) --- desc_start_match = re.match(r'^#\s*Description:\s*(.*)', line_raw, re.IGNORECASE) diff --git a/server/dive_utils/types.py b/server/dive_utils/types.py index e057a6e3a..7bc06cf77 100644 --- a/server/dive_utils/types.py +++ b/server/dive_utils/types.py @@ -59,12 +59,14 @@ class Config: extra = 'forbid' -class DiveParam(TypedDict): +class DiveParam(TypedDict, total=False): label: str type: str type_props: list[str] key: str default: str + # True if the pipeline can't run until the user supplies a value + required: bool class PipeMetadata(TypedDict):