import { noop } from 'lodash'; import React, { ReactNode, useState } from 'react'; import { NavLink } from 'react-router-dom'; import Tooltip from 'wdk-client/Components/Overlays/Tooltip'; import { Plugin } from 'wdk-client/Utils/ClientPlugin'; import { RecordClass } from 'wdk-client/Utils/WdkModel'; import { Step } from 'wdk-client/Utils/WdkUser'; import { makeStepBoxDisplayName } from 'wdk-client/Views/Strategy/StrategyUtils'; import { StepBoxesProps, StepBoxProps, isTransformUiStepTree, isCombineUiStepTree, isCompleteUiStepTree, PartialUiStepTree, isPartialCombineUiStepTree } from 'wdk-client/Views/Strategy/Types'; import StepDetailsDialog from 'wdk-client/Views/Strategy/StepDetailsDialog'; import { cxStepBoxes as cx } from 'wdk-client/Views/Strategy/ClassNames'; import { useBinaryStepBoxClassName } from 'wdk-client/Utils/Operations'; import { TransformIcon } from 'wdk-client/Views/Strategy/TransformIcon'; const INVALID_SEARCH_TITLE = 'This step refers to a search that is no longer valid. In order to fix your strategy, this step must be deleted.'; const INVALID_PARAMS_TITLE = 'This step contains a configuration that is no longer valid and must be revised to view results.' const INVALID_NESTED_TITLE = 'This nested strategy contains a step that is no longer valid. Expand this nested strategy to see which steps need to be updated.' export const ADD_STEP_BUTTON_VERBIAGE = 'Combine the results of your strategy with the results of a new search, or convert the results of your strategy with available data transformations.'; /** * Render each step of a strategy as a grid. */ export default function StepBoxes(props: StepBoxesProps) { return (
) } /** * Recursively render the step tree. Steps are rendered into "slots", where a * slot contains a primary input and its secondary input if it has one (the * left-most rendered step, and transforms, do not have seondary inputs) * * We also close over provided handlers with the appropriate step id and * sequence of actions. */ function StepTree(props: StepBoxesProps) { const { stepTree, isDeleteable = true } = props; if (!isCompleteUiStepTree(stepTree)) { return props.onDeleteStep(stepTree.step.id)}/> } const { step, primaryInput, secondaryInput, question, areInputsValid } = stepTree; // primary input is deleteable if the current step accepts // the record type of the primary input's primary input record type, // or if the primary input does not have a primary input. const primaryInputIsDeletable = primaryInput != null && question.allowedPrimaryInputRecordClassNames != null && ( primaryInput.primaryInput == null || question.allowedPrimaryInputRecordClassNames.includes(primaryInput.primaryInput.step.recordClassName) ) return ( {primaryInput && }
props.onRenameStep(step.id, newName), // no-op; primary inputs cannot be nested makeNestedStrategy: noop, makeUnnestStrategy: noop, collapseNestedStrategy: noop, expandNestedStrategy: noop, showNewAnalysisTab: () => props.onAnalyzeStep(), showReviseForm: () => props.setReviseFormStepId(step.id), insertStepBefore: (selectedOperation?: string, pageHistory?: string[]) => props.onShowInsertStep({ type: 'insert-before', stepId: step.id, selectedOperation, pageHistory }), insertStepAfter: (selectedOperation?: string, pageHistory?: string[]) => props.onShowInsertStep({ type: 'append', stepId: step.id, selectedOperation, pageHistory }), deleteStep: () => props.onDeleteStep(step.id) }} defaultComponent={StepBox} /> {secondaryInput && ( !isCompleteUiStepTree(secondaryInput) ? props.onDeleteStep(secondaryInput.step.id)}/> : ( { if (secondaryInput.isNested) { props.onRenameNestedStrategy(step.id, newName); } else { props.onRenameStep(secondaryInput.step.id, newName); } }, makeNestedStrategy: () => { props.onMakeNestedStrategy(step.id); props.onExpandNestedStrategy(step.id); }, makeUnnestStrategy: () => { props.onMakeUnnestedStrategy(step.id); props.onCollapseNestedStrategy(step.id); // Use empty string to indicate that the step should not be rendered as a nested strategy props.onRenameNestedStrategy(step.id, ''); }, collapseNestedStrategy: () => { props.onCollapseNestedStrategy(step.id); }, expandNestedStrategy: () => { props.onExpandNestedStrategy(step.id); }, showNewAnalysisTab: () => props.onAnalyzeStep(), showReviseForm: () => props.setReviseFormStepId(secondaryInput.step.id), insertStepBefore: (selectedOperation?: string, pageHistory?: string[]) => props.onShowInsertStep({ type: 'insert-before', stepId: step.id, selectedOperation, pageHistory }), insertStepAfter: (selectedOperation?: string, pageHistory?: string[]) => props.onShowInsertStep({ type: 'append', stepId: secondaryInput.step.id, selectedOperation, pageHistory }), deleteStep: () => props.onDeleteStep(step.id, secondaryInput.isNested) }} defaultComponent={StepBox} /> ) ) }
Step {stepTree.slotNumber}
); } export function LeafPreview() { return (
); } export const combinedPreviewFactory = (operatorClassName: string) => () =>
; export const BooleanPreview = combinedPreviewFactory(cx('--CombineOperator', 'PREVIEW')); export function TransformPreview() { return (
); }; function UnknownQuestionStepBox({ stepTree, deleteStep }: { stepTree: PartialUiStepTree, deleteStep: () => void }) { const deleteButton = ( ); return (
{deleteButton}
); } type PreviewStepBoxesProps = { stepTree: PartialUiStepTree, fromSlot: number, toSlot: number, insertType: 'append' | 'insert-before', insertAtSlot: number, newOperationStepBox: ReactNode, newInputStepBox: ReactNode }; const existingStepPreviewProps = { isAnalyzable: false, isExpanded: false, isDeleteable: false, renameStep: noop, makeNestedStrategy: noop, makeUnnestStrategy: noop, collapseNestedStrategy: noop, expandNestedStrategy: noop, showNewAnalysisTab: noop, showReviseForm: noop, insertStepBefore: noop, insertStepAfter: noop, deleteStep: noop }; export function PreviewStepBoxes(props: PreviewStepBoxesProps) { const previewModifier = props.insertType === 'append' ? 'appending-step' : props.insertAtSlot > 1 ? 'inserting-between-steps' : 'prepending-step'; return (
); } /** * Recursively render the step tree. Steps are rendered into "slots", where a * slot contains a primary input and its secondary input if it has one (the * left-most rendered step, and transforms, do not have seondary inputs) * * We also close over provided handlers with the appropriate step id and * sequence of actions. */ function PreviewStepTree(props: PreviewStepBoxesProps) { const { stepTree, fromSlot, toSlot, insertType, insertAtSlot, newInputStepBox, newOperationStepBox } = props; if (!isCompleteUiStepTree(stepTree)) { return ; } const { step, primaryInput, secondaryInput, areInputsValid } = stepTree; const existingStepBox = ( ); const existingStepSecondaryInputBox = ( secondaryInput && ( !isCompleteUiStepTree(secondaryInput) ? : ) ); const shouldRenderExistingSlotContent = fromSlot <= stepTree.slotNumber && stepTree.slotNumber <= toSlot; return ( {primaryInput && } { shouldRenderExistingSlotContent ? :
}
); } type SlotContentProps = { insertType: 'append' | 'insert-before', insertAtSlot: number, currentSlot: number, existingStepBox: ReactNode, existingStepSecondaryInputBox: ReactNode newOperationStepBox: ReactNode, newInputStepBox: ReactNode }; function SlotContent({ insertType, insertAtSlot, currentSlot, existingStepBox, existingStepSecondaryInputBox, newOperationStepBox, newInputStepBox }: SlotContentProps) { if (currentSlot !== insertAtSlot) { return (
{existingStepBox} {existingStepSecondaryInputBox}
Step {currentSlot > insertAtSlot ? currentSlot + 1 : currentSlot}
); } else if (insertType === 'append') { return (
{existingStepBox} {existingStepSecondaryInputBox}
Step {currentSlot}
{newOperationStepBox} {newInputStepBox}
Step {currentSlot + 1}
); } else if (insertAtSlot >= 2) { return (
{newOperationStepBox} {newInputStepBox}
Step {currentSlot}
{existingStepBox} {existingStepSecondaryInputBox}
Step {currentSlot + 1}
); } else { return (
{newInputStepBox}
Step 1
{newOperationStepBox} {existingStepBox}
Step 2
); } } const stepBoxFactory = (isPreview: boolean) => (props: StepBoxProps) => { const { isNested, areInputsValid, stepTree, deleteStep, showReviseForm } = props; const { step, color, primaryInput, secondaryInput } = stepTree; const [ isDetailVisible, setDetailVisibility ] = useState(false); const StepComponent = isCombineUiStepTree(stepTree) && !isNested ? CombineStepBoxContent : isTransformUiStepTree(stepTree) && !isNested ? TransformStepBoxContent : LeafStepBoxContent; const classModifier = isCombineUiStepTree(stepTree) && !isNested ? 'combined' : isTransformUiStepTree(stepTree) && !isNested ? 'transform' : 'leaf'; const nestedModifier = isNested ? 'nested' : ''; const previewModifier = isPreview ? 'existing-step-preview' : ''; const borderColor = isNested && stepTree.nestedControlStep && stepTree.nestedControlStep.expanded ? color : undefined; const allowRevise = !isNested && !isCombineUiStepTree(stepTree); const hasValidSearch = ( (primaryInput == null || isCompleteUiStepTree(primaryInput)) && (secondaryInput == null || isCompleteUiStepTree(secondaryInput)) ); const editButton = isPreview ? null : hasValidSearch || isNested ? ( ) : ( ) const stepDetails = isPreview ? null : setDetailVisibility(false)}/>; const filterIcon = step.isFiltered && ( ); const title = !hasValidSearch ? INVALID_SEARCH_TITLE : !step.validation.isValid ? INVALID_PARAMS_TITLE : isNested && !areInputsValid ? INVALID_NESTED_TITLE : undefined; const isValid = step.validation.isValid && (isNested ? areInputsValid : true); return (
{ if (isPreview) { e.preventDefault(); } }} > {editButton} {stepDetails} {filterIcon}
); } const StepBox = stepBoxFactory(false); const ExistingStepPreviewBox = stepBoxFactory(true); function LeafStepBoxContent(props: StepBoxProps) { const { stepTree, isNested } = props; const { step, recordClass, nestedControlStep } = stepTree; return ( ); } function TransformStepBoxContent(props: StepBoxProps) { const { step, recordClass } = props.stepTree; return (
); } function CombineStepBoxContent(props: StepBoxProps) { const { step, recordClass } = props.stepTree; return ( ); } function CombinedStepIcon(props: { step: Step }) { const { step } = props; const operatorClassName = useBinaryStepBoxClassName(step); return (
); } function StepName(props: { step: Step, isCombine: boolean, nestedControlStep?: Step }) { const { step, isCombine, nestedControlStep } = props; const displayName = makeStepBoxDisplayName(step, isCombine, nestedControlStep); return
{displayName}
; } function StepCount(props: { step: Step, recordClass?: RecordClass }) { const { step, recordClass } = props; const recordClassDisplayName = recordClass && ( step.estimatedSize === 1 ? recordClass.shortDisplayName : recordClass.shortDisplayNamePlural ); const count = step.estimatedSize != null ? step.estimatedSize.toLocaleString() : '?'; return
{count} {recordClassDisplayName}
} /** * Recurisvely render expanded steps */ export function ExpandedSteps(props: StepBoxesProps) { const { stepTree } = props; if (stepTree == null || stepTree == null) return null; return ( {stepTree.primaryInput && } {stepTree.secondaryInput && stepTree.secondaryInput.isNested && stepTree.step.expanded && (
Expanded view of {makeStepBoxDisplayName(stepTree.secondaryInput.step, isPartialCombineUiStepTree(stepTree.secondaryInput), stepTree.step)}
)}
); }