Skip to content

Conversation

hikchoi
Copy link
Contributor

@hikchoi hikchoi commented Sep 6, 2021

feat: consolidate dendron configs

This PR consolidates and organizes all Dendron configurations.

Docs for config style guide: dendronhq/dendron-site#196
Docs for diff between as-is config and to-be config: TBD

for easier review, read in this order

  1. Overview of changes below
  2. Config convention docs PR: add convention guide for configs dendron-site#196
  3. packages/common-all/src/types/configs/base.ts
  4. everything in packages/common-all/src/types/configs/*, starting from packages/common-all/src/types/configs/dendronConfig.ts
  5. everything in packages/common-all/src/constants/configs/*, starting form packages/common-all/src/constants/configs/dendronConfig.ts

Overview of changes

The new configuration is organized into the following namespaces:

  • global (DendronGlobalConfig)
    • config entries that can affect both preview and publishing.
    • all config entries here are also in the preview and publishing namespace.
    • configurations set here will be overriden by a more specific config entry if it exists.
  • preview (DendronPreviewConfig)
    • config entries that changes behavior of preview.
  • publishing (DendronPublishingConfig)
    • config entries that changes behavior of publishing.
  • workspace (DendronWorkspaceConfig)
    • config entries that changes behavior of the workspace.
  • commands (DendronCommandConfig)
    • config entries that change behavior of Dendron commands.
  • dev (DendronDevConfig)
    • beta feature flags.

Config type definitions

  • The type definition of these configurations can be found in their respective modules in packages/common-all/src/types/configs/ along with a function that generates the defaults of those configs.
  • Some top level namespaces are further divided in to smaller sub-namespaces so the configurations are logically nested into groups.
  • All namespaces are defined to be required with the exception of DendronDevConfig.

Constants

  • Each namespace also has a constant object that holds labels and descriptions about every possible configuration entries.
  • They will be used to generate the configuration UI dynamically.
  • These can be found in packages/common-all/src/constants/configs/
  • Every config entry that is not a namespace (i.e. a single config) will have a constant of type DendronConfigEntry<T>, where T is the type of the config value.
  • For example, for the config initialValue of the command InsertNote has a DendronConfigEntry<string> like so:
      {
        label: "Initial Value",
        desc: "Initial value that will be filled when prompted.",
      },
  • This is the type definition of DendronConfigEntry:
    export type DendronConfigEntry<T> = {
      value?: T;
      label: string;
      desc: string;
    };
    • Note that value is defined to be optional, as some configs can have arbitrary values. In which case passing in the correct type T is meaningless, but is there for completeness' sake.
  • If a DendronConfigEntry have a selected number of possible values, the value is specified, and label and description is written to take account of the different behaviors respective values enable.
  • For example, the selection mode extract of Note lookup looks like this:
    {
      value: LookupSelectionModeEnum.extract,
      label: "Extract Selection",
      desc: "Extract selection of active editor and use it as body of new note.",
    },
  • Some configurations are duplicated in multiple namespaces, thus producing identical DendronConfigEntry in multiple places. Some configurations benefit from dynamically generated descriptions. To reduce repeated code, there are functions that generate DendronConfigEntry based on the value given.
  • For example, enabling mermaid syntax can happen in preview and publishing (as well as global). This is the function that generates the config entries.
    export const ENABLE_MERMAID = (
      namespace: TopLevelDendronConfig
    ): DendronConfigEntry<boolean> => {
      return {
        label: `Enable Mermaid (${namespace})`,
        desc: `Enable the use of mermaid for rendering diagrams. (${namespace})`,
      };
    };
    
    // used elsewhere
    {
      ...
      enableMermaid: ENABLE_MERMAID("preview"),
      ...
    }
  • Like the type definition, the constants are nested into groups of namespaces.
  • In fact, the type signature of these constants is a mapped type that maps all the config type's keys into DendronConfigEntrys.
    • This is called a DendronConfigEntryCollection.
    • The benefit of this is that if you change the definition of any type in any namespace, you will get an error that you forgot to define the DendronConfigEntry of it if you omit it.
  • DendronConfigEntryCollection looks like this:
    export type DendronConfigEntryCollection<T> = {
      [Property in keyof T]-?: // the -? tells the compiler that we don't want to ignore optionals
        | DendronConfigEntryCollection<any>
        | Record<any, DendronConfigEntry<any>>
        | DendronConfigEntry<any>
        | ((...args: any[]) => DendronConfigEntry<any>);
    };
    • It is either:
      • Another DendronConfigEntryCollection (of a sub-namespace),
      • A Record that maps possible config enum keys to DendronConfigEntry
        • think NoteLookup's selection mode.
      • A single DendronConfigEntry
      • A function that produces DendronConfigEntry based on what value it is given.

Conventions

  • To maintain consistency in the configuration space, a few conventions have been established.
  • PR to the convention guide can be found here
  • Existing configurations that do not conform to these conventions are changed in this PR.

Pull Request Checklist

You can go to dendron pull requests to see full details for items in this checklist.

General

Quality Assurance

  • [~] add a test for the new feature
  • make sure all the existing tests pass
  • [~] do a spot check by running your feature with our test workspace
  • after you submit your pull request, check the output of our integration test and make sure all tests pass
    • NOTE: if you running mac/linux, check the windows output and vice versa if you are developing on windows

Special Cases

  • [~] if your tests changes an existing snaphot, make sure that snapshots are updated
  • [~] if you are adding a new language feature (graphically visible in vscode/preview/publishing), make sure that it is included in test-workspace. We use this to manually inspect new changes and for auto regression testiing

Docs

  • Make sure that the PR title follows our commit style
  • Please summarize the feature or impact in 1-2 lines in the PR description
  • [.] If your change reflects documentation changes, also submit a PR to dendron-site and mention the doc PR link in your current PR

@hikchoi hikchoi marked this pull request as draft September 6, 2021 11:57
Copy link
Member

@kevinslin kevinslin left a comment

Choose a reason for hiding this comment

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

This looks great. Left some minor comments but go ahead with this. For this commit, is the plan to carry out phase I of config migration? Can we have this ready to be merged for next week's release?

For deprecation notice, since we'll do automatic migration, we might not need to do any sort of deprecation notice besides announcing it in our release notes. FOr migrating to the new configuration, can we do it piecemeal?

Eg. Week1 -> migrate command config. Week2 -> migrate 2 more sections. Week3 -> migrate everything...

/**
* Constants holding all command config related {@link DendronConfigEntry}
*/
export const COMMANDS = {
Copy link
Member

Choose a reason for hiding this comment

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

thoughts between using constants vs enums? You can use Object.values to get all enum values

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The values in the object are also constants that themselves holding other constants or functions that produce constants.

Are you suggesting we have nested enums (not sure if that's a thing)? or we flatten it out and have a giant enum that holds all possible labels and descriptions?

* @param value {@link dayOfWeekNumber}
* @returns DendronConfigEntry
*/
const FIRST_DAY_OF_WEEK = (
Copy link
Member

Choose a reason for hiding this comment

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

just a heads up that we're currently not supporting this option due to limitation in calendar widget. we'll add support back in eventually but FYI

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am aware of this. I also have been taking note on other configs with some peculiarities that we need to address once we start actually migrating.

@hikchoi
Copy link
Contributor Author

hikchoi commented Sep 10, 2021

This looks great. Left some minor comments but go ahead with this. For this commit, is the plan to carry out phase I of config migration? Can we have this ready to be merged for next week's release?

For deprecation notice, since we'll do automatic migration, we might not need to do any sort of deprecation notice besides announcing it in our release notes. FOr migrating to the new configuration, can we do it piecemeal?

Eg. Week1 -> migrate command config. Week2 -> migrate 2 more sections. Week3 -> migrate everything...

Yeah, I think it makes sense to announce what's going to change and then slowly migrate the configurations to the new one.
There are a lot of things to fix (splitting the effect of config so that the dupes in multiple areas are respected) so a piecemeal migration is likely better than one giant one.

I'll get it ready by next release, and also write up an announcement to add to the release notes of what the changes will entail.

@hikchoi
Copy link
Contributor Author

hikchoi commented Sep 13, 2021

UPDATE:

Please check the overview in the main body of this PR. Since this PR is waaaay too big, I go over the general structure of what is being added.

This PR does not change any existing behavior of the config. It just adds the newly organized config modules.
Over the next few weeks I will incrementally migrate the new configs in chunks.
Due to potential package namespace collision issues, I have not exported any of the newly added modules yet; they will be exported from @dendronhq/common-all as I actually migrate the configs.

I will also be adding a document that goes over exactly which config is going to be mapped to which in a separate docs PR, which can be added to the upcoming release notes.

@hikchoi hikchoi marked this pull request as ready for review September 13, 2021 14:18
@auto-assign auto-assign bot requested a review from SeriousBug September 13, 2021 14:18
@hikchoi
Copy link
Contributor Author

hikchoi commented Sep 14, 2021

Here is an exhaustive list of changes to every config key

  • This can also be found in [[Diff of All Changes|user.hikchoi.scratch.2021.09.14.diff-of-all-changes]]

Dendron Configurations

  • noCaching?: boolean;
    • Renamed to enableCaching
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.enableCaching
  • maxPreviewsCached?: number;
    • Now a required property
    • Now explicitly defaults to the value 10
    • New location: DendronConfig.workspace.maxPreviewsCached
  • noTelemetry?: boolean;
    • Renamed to enableTelemetry
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.enableTelemetry
  • version: number;
    • deprecated
  • dendronVersion?: string;
    • New location: DendronConfig.workspace.dendronVersion
  • site: DendronSiteConfig;
    • See Site Configurations below
  • lookup: LookupConfig;
    • New location: DendronConfig.commands.lookup
    • lookup.note.selectionType renamed to lookup.note.selectionMode
    • enum values of selection modes are renamed
      • selectionExtract renamed to extract
      • selection2link renamed to link
  • journal: JournalConfig;
    • New location: DendronConfig.workspace.journal
    • journal.firstDayOfWeek temporarily removed
  • scratch?: ScratchConfig;
    • Now a required property
    • New location: DendronConfig.workspace.scratch
  • insertNoteLink?: InsertNoteLinkConfig;
    • Now a required property
    • aliasMode now explicitly defaults to none
    • enableMultiSelect now explicitly defaults to false
    • New location: DendronConfig.commands.insertNoteLink
    • insertNoteLinkConfig.multiselect renamed to insertNoteLink.enableMultiSelect
  • workspaces?: { [key: string]: DWorkspaceEntry | undefined };
    • New location: DendronConfig.workspace.workspaces
  • seeds?: { [key: string]: SeedEntry | undefined };
    • New location: DendronConfig.workspace.seeds
  • vaults: DVault[];
    • New location: DendronConfig.workspace.vaults
  • hooks?: DHookDict;
    • New location: DendronConfig.workspace.hooks
  • lookupConfirmVaultOnCreate?: boolean;
    • Now part of lookupConfig
    • Renamed to confirmVaultOnCreate
    • Now a required property
    • Now explicitly defaults to false
    • New location: DendronConfig.commands.lookup.note.confirmVaultOnCreate
  • useFMTitle?: boolean;
    • Renamed to enableFMTitle
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • useNoteTitleForLink?: boolean;
    • Renamed to enableNoteTitleForLink
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • mermaid?: boolean;
    • Renamed to enableMermaid
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • useNunjucks?: boolean;
    • Renamed to enableNunjucks
    • Now a required property in global namespace.
    • Now explicitly set to false in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • usePrettyRefs?: boolean;
    • Renamed to enablePrettyRefs
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • useKatex?: boolean;
    • Renamed to enableKatex
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • hierarchyDisplay?: boolean;
    • Renamed to enabledHierarchyDisplay
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • hierarchyDisplayTitle?: string;
    • Now explicitly set to children only in global namespace, but remains optional.
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • graph?: DendronGraphConfig;
    • Now a required property
    • zoomSpeed explicitly defaults to the value 1
    • New location: DendronConfig.workspace.graph
  • noAutoCreateOnDefinition?: boolean;
    • Renamed to enableAutoCreateOnDefinition
    • Now a require property
    • Now explicitly defaults to false
    • New location: DendronConfig.workspace.enableAutoCreateOnDefinition
  • noLegacyNoteRef?: boolean;
    • Renamed to enableLegacyNoteRef
    • Now a required property in global namespace.
    • Now explicitly set to false in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace
  • noXVaultWikiLink?: boolean;
    • Renamed to enableXVaultWikiLink
    • Now a required property
    • Now explicitly defaults to false
    • New location: DendronConfig.workspace.enableXVaultWikiLink
  • initializeRemoteVaults?: boolean;
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.initializeRemoteVault
  • feedback?: boolean;
    • New location: DendronConfig.workspace.feedback
  • apiEndpoint?: string;
    • New location: DendronConfig.workspace.apiEndpoint
  • defaultInsertHierarchy?: string;
    • Now part of insertNoteConfig
    • renamed to initialValue
    • Explicitly defaults to templates but remains optional
    • New location: DendronConfig.commands.insertNote.initialValue
  • dev?: DendronDevConfig;
    • New location: DendronConfig.dev
  • workspaceVaultSync?: DVaultSync;
    • Renamed to workspaceVaultSyncMode
    • Now a required property
    • Now explicitly defaults to noCommit
    • New location: DendronConfig.workspace.workspaceVaultSyncMode
  • randomNote?: RandomNoteConfig;
    • Now a required property
    • Properties randomNote.include and randomNote.exclude remains optional
    • New location: DendronConfig.commands.randomNote
  • autoFoldFrontmatter?: boolean;
    • Renamed to enableAutoFoldFrontmatter
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.enableAutoFoldFrontmatter
  • insertNoteIndex?: InsertNoteIndexConfig;
    • Now a required property
    • New location: DendronConfig.commands.insertNoteIndex
    • insertNoteIndexConfig.marker renamed to insertNoteIndexConfig.enableMarker
    • insertNoteIndexConfig.enableMarker is now a required property
    • insertNoteIndexConfig.enableMarker now explicitly defaults to false
  • maxNoteLength?: number;
    • Now a required property
    • Now explicitly defaults to the value 204800
    • New location: DendronConfig.workspace.maxNoteLength

Site Configurations

  • assetsPrefix?: string;
    • New location: DendronConfig.publishing.assetsPrefix
  • canonicalBaseUrl?: string;
    • New location: DendronConfig.publishing.canonicalBaseUrl
  • copyAssets?: boolean;
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.publishing.copyAssets
  • customHeaderPath?: string;
    • New location: DendronConfig.publishing.customHeaderPath
  • ga_tracking?: string;
    • Now part of GoogleAnalyticsConfig
    • New location: DendronConfig.publishing.ga.tracking
  • siteFaviconPath?: string;
    • New location: DendronConfig.publishing.siteFaviconPath
  • logo?: string;
    • Renamed to logoPath
    • New location: DendronConfig.publishing.logoPath
  • siteIndex?: string;
    • New location: DendronConfig.publishing.siteIndex
  • siteHierarchies: string[];
    • New location: DendronConfig.publishing.siteHierarchies
  • siteLastModified?: boolean;
    • Renamed to enableSiteLastModified
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.publishing.enableSiteLastModified
  • siteRootDir: string;
    • New location: DendronConfig.publishing.siteRootDir
  • siteRepoDir?: string;
    • New location: DendronConfig.publishing.siteRepoDir
  • siteNotesDir?: string;
    • deprecated
  • siteUrl?: string;
    • New location: DendronConfig.publishing.siteUrl
  • githubCname?: string;
    • Now part of GithubConfig
    • Renamed to cname
    • New location: DendronConfig.publishing.github.cname
  • gh_edit_link?: string;
    • Now part of GithubConfig
    • Renamed to enableEditLink
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.publishing.github.enableEditLink
  • gh_edit_link_text?: string;
    • Now part of GithubConfig
    • Renamed to editLinkText
    • Remains optional but explicitly defaults to Edit this page on GitHub
    • New location: DendronConfig.publishing.github.editLinkText
  • gh_edit_branch?: string;
    • Now part of GithubConfig
    • Renamed to editBranch
    • Remains optional but explicitly defaults to main
    • New location: DendronConfig.publishing.github.editBranch
  • gh_edit_view_mode?: "tree" | "edit";
    • Now part of GithubConfig
    • Renamed to editViewMode
    • Remains optional but explicitly defaults to tree
    • New location: DendronConfig.publishing.github.editViewMode
  • gh_edit_repository?: string;
    • Now part of GithubConfig
    • Renamed to editRepository
    • New location: DendronConfig.publishing.github.editRepository
  • usePrettyRefs?: boolean;
    • Renamed to enablePrettyRefs
    • Now a required property in global namespace.
    • Now explicitly set to true in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated in global, preview, and publishing
  • hideBlockAnchors?: boolean;
    • deprecated
  • showFrontMatterTags?: boolean;
    • Renamed to enableFrontmatterTags
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.publishing.enableFrontmatterTags
  • noRandomlyColoredTags?: boolean;
    • Renamed to enableRandomlyColoredTags
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.publishing.enableRandomlyColoredTags
  • config?: { [key: string]: HierarchyConfig };
    • Renamed to hierarchy
    • New location: DendronConfig.publishing.hierarchy
  • duplicateNoteBehavior?: DuplicateNoteBehavior;
    • New location: DendronConfig.publishing.duplicateNoteBehavior
  • writeStubs?: boolean;
    • Now a required property
    • Now explicitly defaults to false
    • New location: DendronConfig.publishing.writeStubs
  • title?: string;
    • Now part of SEOConfig
    • New location: DendronConfig.publishing.seo.title
  • description?: string;
    • Now part of SEOConfig
    • New location: DendronConfig.publishing.seo.description
  • author?: string;
    • Now part of SEOConfig
    • New location: DendronConfig.publishing.seo.author
  • twitter?: string;
    • Now part of SEOConfig
    • New location: DendronConfig.publishing.seo.twitter
  • image?: string;
    • Now part of SEOConfig
    • New location: DendronConfig.publishing.seo.image
  • useContainers?: boolean;
    • Renamed to enableContainers
    • Now a required property
    • Now explicitly defaults to false
    • New location: DendronConfig.publishing.enableContainers
  • generateChangelog?: boolean;
    • Now a required property
    • Now explicitly defaults to false
    • New location: DendronConfig.publishing.generateChangelog
  • previewPort?: boolean;
    • Type fixed to number
    • New location: DendronConfig.publishing.previewPort
  • segmentKey?: string;
    • New location: DendronConfig.publishing.segmentKey
  • cognitoUserPoolId?: string;
    • New location: DendronConfig.publishing.cognitoUserPoolId
  • cognitoClientId?: string;
    • New location: DendronConfig.publishing.cognitoClientId
  • usePrettyLinks?: boolean;
    • Renamed to enablePrettyLinks
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.publishing.enablePrettyLinks

@kevinslin
Copy link
Member

Great job on putting in the ground work. Configuration renames look good, just a few changes suggested below

  • noCaching?: boolean;

    • Renamed to enableCaching
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.enableCaching

    we initially had this because we weren't sure if caching would break things. it hasn't so far - lets just remove this entirely

  • noTelemetry?: boolean;

    • Renamed to enableTelemetry
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.enableTelemetry

    can we change this to disableTelemetry and keep it as optional?

  • noLegacyNoteRef?: boolean;

    • Renamed to enableLegacyNoteRef
    • Now a required property in global namespace.
    • Now explicitly set to false in global namespace.
    • Remains optional in preview and publishing
    • When explicitly set in preview or publishing namespace, it will have precedence over the value set in global namespace.
    • Now duplicated across global, preview, and publising namespace

    we should just disable this altogether and remove the option

  • initializeRemoteVaults?: boolean;

    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.initializeRemoteVault

    change to enableRemoteVaultInit

  • autoFoldFrontmatter?: boolean;

    • Renamed to enableAutoFoldFrontmatter
    • Now a required property
    • Now explicitly defaults to true
    • New location: DendronConfig.workspace.enableAutoFoldFrontmatter

    this has actually caused a bunch of issues. can we change the default to false?

@kevinslin
Copy link
Member

Note that value is defined to be optional, as some configs can have arbitrary values. In which case passing in the correct type T is meaningless, but is there for completeness' sake.

typescript supports initializing generics, would rewrite to the following in that case

export type DendronConfigEntry<T=any> = {
  value?: T;
  label: string;
  desc: string;
};

Copy link
Member

@kevinslin kevinslin left a comment

Choose a reason for hiding this comment

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

see config changes in conversation

* For config entries that can be an arbitrary value, only specify the label and description.
* For config entries that have pre-defined choices, provide the value as well as label and description specific to that value.
*/
export type DendronConfigEntry<T> = {
Copy link
Member

Choose a reason for hiding this comment

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

since value is optional we can make T=any to cut down on boilerplate

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TIL 😅 still so much to learn about ts.

@kevinslin kevinslin merged commit 177ac92 into master Sep 17, 2021
@kevinslin kevinslin deleted the feat/consolidate-dendron-configs branch September 17, 2021 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants