<NME> workflow-card.ts
<BEF> import { action } from '@ember/object';
import { Participant, WorkflowPostable } from './workflow-postable';
import { IWorkflowSession } from './workflow-session';

export interface WorkflowCardComponentArgs {
  workflowSession: IWorkflowSession;
  onComplete: (() => void) | undefined;
  onIncomplete: (() => void) | undefined;
  isComplete: boolean;
}

type SuccessCheckResult = {
  success: true;
};

type FailureCheckResult = {
  success: false;
  reason: string;
};

export type CheckResult = SuccessCheckResult | FailureCheckResult;

interface WorkflowCardOptions<T extends string> {
  cardName?: string;
  author?: Participant;
  componentName: T; // this should eventually become a card reference
  includeIf?(this: WorkflowCard<T>): boolean;
  check?(this: WorkflowCard<WorkflowCardOptions>): Promise<CheckResult>;
}

export interface ConfigurableWorkflowCardOptions extends WorkflowCardOptions {
  componentName: keyof CardConfiguration; // this should eventually become a card reference
  includeIf?(this: WorkflowCard<ConfigurableWorkflowCardOptions>): boolean;
 */
type ObjectInheritingValueOptionality<
  ): Promise<CheckResult>;
}

export class WorkflowCard<
  T extends
    | ConfigurableWorkflowCardOptions
   * declare module '@cardstack/web-client/models/workflow/workflow-card' {
   *   interface CardConfiguration {
   *     '<what you would pass to the component helper>': {
   *       // ...whatever config you need
   *     };
   *   }
   * }
  };

  /**
   * ConfigurableWorkflowCardOptions is a set of options with componentName registered in the CardConfiguration interface
   * WorkflowCardOptions is a set of options without the componentName registered in the CardConfiguration interface
   *
   * This constructor checks if the componentName is registered in the CardConfiguration interface, and if so, whether the componentName's
   * corresponding type in that interface is optional or not.
   *
   *     '<what you would pass to the component helper>'?: {
   *       // ...whatever config you need
   *     };
   *   }
   * }
   * ```
   */
  constructor(
    options: WorkflowCardOptions<
      T &
        (T extends keyof CardConfiguration
          ? // note that this string here is an exception to detection for being in the card configuration registry
            // so if you do 'card-pay/layer-one-connect-card' as the string, we won't be able to detect whether it's in the registry
            'You need config for this item'
          : {})
    >
  );
  constructor(
    options: T extends keyof CardConfiguration
      ? WorkflowCardOptions<T> &
          ObjectInheritingValueOptionality<'config', CardConfiguration[T]>
      : never
  );
  constructor(
    options: WorkflowCardOptions<T> & {
      config?: T extends keyof CardConfiguration ? CardConfiguration[T] : never;
   * }
   * ```
   */
  constructor(
    options: T extends ConfigurableWorkflowCardOptions
      ? T &
          (undefined extends CardConfiguration[T['componentName']]
            ? {
                config?: CardConfiguration[T['componentName']];
              }
            : { config: CardConfiguration[T['componentName']] })
      : never | WorkflowCardOptions
  ) {
    super(options.author, options.includeIf);
    this.componentName = options.componentName!;
    this.cardName = options.cardName || '';

    this.reset = () => {
      if (this.isComplete) {
        this.isComplete = false;
      }
    };
    if (options.check) {
      this.check = options.check as this['check'];
    }
  }

  get session(): IWorkflowSession | undefined {
    return this.workflow?.session;
  }

  get isEditable() {
    return this.editableIf ? this.editableIf(this.workflow!.session) : true;
  }

  get completedCardNames(): Array<string> {
    return this.session?.getMeta()?.completedCardNames ?? [];
  }

  hasConfig(
    options: any
  ): options is { config: CardConfiguration[keyof CardConfiguration] } {
    return Object.keys(options).includes('config');
  }

  @action async onComplete() {
    if (this.isComplete) return;
    let checkResult = await this.check();
    if (checkResult.success) {
      // visible-postables-will-change starts test waiters in animated-workflow.ts
      this.workflow?.emit('visible-postables-will-change');
      this.isComplete = true;
    } else {
      this.workflow?.cancel(checkResult.reason);
    }

    if (this.isComplete && this.cardName) {
      if (!this.completedCardNames.includes(this.cardName)) {
        this.session?.setMeta({
          completedCardNames: [...this.completedCardNames, this.cardName],
          completedMilestonesCount: this.workflow?.completedMilestoneCount,
          milestonesCount: this.workflow?.milestones.length,
        });
      }
    }
  }

  @action onIncomplete() {
    this.workflow?.resetTo(this);

    if (this.cardName && this.completedCardNames.length > 0) {
      const resetToIndex = this.completedCardNames.indexOf(this.cardName);

      this.session?.setMeta({
        completedCardNames: this.completedCardNames.slice(0, resetToIndex),
        completedMilestonesCount: this.workflow?.completedMilestoneCount,
        milestonesCount: this.workflow?.milestones.length,
      });
    }
  }
}

export interface CardConfiguration {}

<MSG> clean up type definition of card configuration check

<DFF> @@ -28,6 +28,9 @@ export interface WorkflowCardOptions {
   check?(this: WorkflowCard<WorkflowCardOptions>): Promise<CheckResult>;
 }
 
+/**
+ * This type is used to check if a card's configuration is registered or not
+ */
 export interface ConfigurableWorkflowCardOptions extends WorkflowCardOptions {
   componentName: keyof CardConfiguration; // this should eventually become a card reference
   includeIf?(this: WorkflowCard<ConfigurableWorkflowCardOptions>): boolean;
@@ -36,6 +39,28 @@ export interface ConfigurableWorkflowCardOptions extends WorkflowCardOptions {
   ): Promise<CheckResult>;
 }
 
+/**
+ * This will create an object type with all keys in `Keys`,
+ * which require a type equal to `Value`.
+ *
+ * The properties are optional if it is possible for Value to be undefined (eg. "a" | "b" | undefined)
+ */
+type ObjectInheritingValueOptionality<
+  Keys extends string,
+  Value
+> = undefined extends Value
+  ? { [key in Keys]?: Value }
+  : { [key in Keys]: Value };
+
+export type ConfigurableIfRegistered<T extends WorkflowCardOptions> =
+  T extends ConfigurableWorkflowCardOptions
+    ? T &
+        ObjectInheritingValueOptionality<
+          'config',
+          CardConfiguration[T['componentName']]
+        >
+    : never | T;
+
 export class WorkflowCard<
   T extends
     | ConfigurableWorkflowCardOptions
@@ -49,9 +74,6 @@ export class WorkflowCard<
   };
 
   /**
-   * ConfigurableWorkflowCardOptions is a set of options with componentName registered in the CardConfiguration interface
-   * WorkflowCardOptions is a set of options without the componentName registered in the CardConfiguration interface
-   *
    * This constructor checks if the componentName is registered in the CardConfiguration interface, and if so, whether the componentName's
    * corresponding type in that interface is optional or not.
    *
@@ -84,16 +106,7 @@ export class WorkflowCard<
    * }
    * ```
    */
-  constructor(
-    options: T extends ConfigurableWorkflowCardOptions
-      ? T &
-          (undefined extends CardConfiguration[T['componentName']]
-            ? {
-                config?: CardConfiguration[T['componentName']];
-              }
-            : { config: CardConfiguration[T['componentName']] })
-      : never | WorkflowCardOptions
-  ) {
+  constructor(options: ConfigurableIfRegistered<T>) {
     super(options.author, options.includeIf);
     this.componentName = options.componentName!;
     this.cardName = options.cardName || '';

{'addition_count': 26, 'commit_subject': 'clean up type definition of card configuration check', 'deletion_count': 13, 'file_extension': '.ts', 'lang': 'ts', 'license': 'mit', 'repo_name': 'cardstack/cardstack'}