NEAR-DevHub / neardevhub-bos

DevHub Portal Product UI (Hosted BOS) – Includes other instances (e.g. Infrastructure, Events)
https://neardevhub.org
MIT License
24 stars 23 forks source link

Introduce community plugin management framework #253

Closed petersalomonsen closed 1 year ago

petersalomonsen commented 1 year ago

Product User Story

Problem Currently, community admins do not have a consistent and easy way to personalize their community experience. For example, the GitHub and Kanban components always display, and an admin must manage them within the tab and cannot remove them.

User Story As an admin, I need a consistent and intuitive method to add, configure, and remove add-ons for my community via the community settings page.

Acceptance Criteria


Old Technical Notes

Is your feature request related to a problem? Please describe. Currently a DevHUB community only allows built-in pages/components like Telegram and Github, limiting the potential for unique functionalities tailored to the needs of each community. Also the current way of adding Telegram and Github is by registering handles/URLs in the central configuration section, which does not give the experience of a "pluggable" component with its own configuration.

Describe the solution you'd like I would like Telegram, Github and BOS widgets in general to be components that can be added from the configuration page, and also have their own configuration sections. A user experience with a button for adding a "widget" like the following screenshot:

image

When clicking "Add widget" you should be able to select from a drop-down menu different alternatives of built-in (like Telegram and Github) as well of community provided widgets:

image

Finally when a custom widget like the "Music tracker" is added, there's a new configuration section in the community admin:

image

and a new tab with the widget in the community user page:

image

Example use cases

An example use case is creating a tab with custom activity feed, so let’s say you pick the Feed widget from the as a component to add, and then it has a configuration called filter, you out filter of tag let’s say blogs and the community name, so now you have a feed of blogs for this community. The tab is renamed to "Blog".

You could even add the feed widget again to another tab, and now with a different filter configuration. For example now you want all the posts from dev-dao, and so you configure the widget with the filter dev-dao. Also you rename that tab with the name Dev DAO.

So now you have the Feed widget present twice, but with different settings.

Additional context

frol commented 1 year ago

Configuration for each community component should be stored to SocialDB ( no need for changes in DevHUB contract )

@petersalomonsen How do you plan to enable editing of that by other community moderators and DevHub moderators? Communities design on SocialDB is not finalized yet: https://github.com/near/near-discovery/discussions/342, so I would suggest to keep using DevHub's contract. It is not hard to add extra fields there.

petersalomonsen commented 1 year ago

@frol I was thinking that we could use the permission feature of SocialDB: https://github.com/NearSocial/social-db#permissions. I have never tried this myself, but from my understanding of the documentation this seems to allow granting permission for any public key.

But of course we can use DevHUB contract too, I was just assuming here that using SocialDB was more straightforward.

frol commented 1 year ago

@petersalomonsen Since we don’t have community accounts, and we don’t want to store community-related keys under the community creator’s account namespace, it is easier to use DevHub contract for now.

P.S. permissions in SocialDB are not revokable, which is another problem to solve as list of moderators is dynamic.

elliotBraem commented 1 year ago

@frol, @petersalomonsen I'd recommend exploring a model like this; we cannot (and should not) control the things people create, but we can control the things we show.

Let's continue the conversation on BOS?

petersalomonsen commented 1 year ago

@frol good point that permissions in SocialDB are not revokable. I've changed the description so that we'll use the DevHub contract for now.

elliotBraem commented 1 year ago

Here is the current structure of a community in the neardevhub-contract:

pub struct CommunityFeatureFlags {
    pub telegram: bool,
    pub github: bool,
    pub board: bool,
    pub wiki: bool,
}

pub struct Community {
    pub admins: Vec<AccountId>,
    pub handle: CommunityHandle,
    pub name: String,
    pub tag: String,
    pub description: String,
    pub logo_url: String,
    pub banner_url: String,
    pub bio_markdown: Option<String>,
    pub github_handle: Option<String>,
    pub telegram_handle: Vec<String>,
    pub twitter_handle: Option<String>,
    pub website_url: Option<String>,
    /// JSON string of github board configuration
    pub github: Option<String>,
    /// JSON string of kanban board configuration
    pub board: Option<String>,
    pub wiki1: Option<WikiPage>,
    pub wiki2: Option<WikiPage>,
    pub features: CommunityFeatureFlags,
}

These are most all L1 fields, they are not grouped by feature, and do not necessarily reflect the structure of the community page itself. This poses as a challenge when considering the ability to dynamically add tabs. Remember this structure while considering the code changes below.

Code changes for implementing this customizable feature would happen in the following files:

A configurator looks like this:

const CommunityWikiPageSchema = {
  name: {
    label: "Name",
    order: 1,
  },

  content_markdown: {
    format: "markdown",
    label: "Content",
    multiline: true,
    order: 2,
  },
};

{widget("components.organism.configurator", {
  heading: "Wiki page 2",
  data: state.communityData?.wiki2,
  isSubform: true,
  isUnlocked: permissions.can_configure,
  onSubmit: (value) => sectionSubmit({ wiki2: value }),
  submitLabel: "Accept",
  schema: CommunityWikiPageSchema,
})}

Each configurator needs a header, destination in parent state, and a schema in order to dynamically create the form from it.

Currently, in order to show a tab, we check if the feature is enabled on the contract or if the fields necessary exist. Then, we need an icon, a title, and a route, which is a relative term for widgets within the same gigs-board directory. We can use the params object to pass props to the route.

const tabs = [
    ...(!community?.features.telegram ||
    (community?.telegram_handle.length ?? 0) === 0
      ? []
      : [
          {
            iconClass: "bi bi-telegram",
            route: "community.telegram",
            title: "Telegram",
          },
        ]),
]

{tabs.map(({ defaultActive, params, route, title }) =>
          title ? (
            <li className="nav-item" key={title}>
              <a
                aria-current={defaultActive && "page"}
                className={[
                  "d-inline-flex gap-2",
                  activeTabTitle === title ? "nav-link active" : "nav-link",
                ].join(" ")}
                href={href(route, { handle, ...(params ?? {}) })}
              >
                <span>{title}</span>
              </a>
            </li>
          ) : null
        )}

Since this new feature would be gated by a "upgrade community button", we can prepare the contract to more closely reflect the community UI. For example, a possible implementation would be:

pub struct CommunityFeature {
    pub title: String,
    pub description: String,
    pub icon: String, // bootstrap icon
    pub src: String, // widget source of custom component
    pub schema: String, // JSON string of schema
    pub parameters: String, // JSON string of specific parameters to be passed to component, follows the schema
    pub enabled: bool // toggle to make tab visible or not, alternative to deleting
}

pub struct Community {
    version: String, // introduce versioning
    ... existing fields
    pub feature_list: Vec<CommunityFeature>
}

This would group the feature fields, replace the feature flags implementation, and make the contract more extendible/modular. Now, gigs-board.entity.community.configurator would be less hard coded, and more reflective of the data stored on the contract. The same goes for the tabs. We can migrate the existing Telegram, Github, Wiki features to fit this new pattern, and so they can be individually added or removed, too.

In summary:

1) Changes to the DevHub contract for tabs to reflect the features 2) Adding button to "upgrade community" to utilize adjusted contract 3) Hardcoding initial custom components and their schemas 4) Adding select button to initialize the configurator based on the schemas 5) Modifying configurator and header "organisms" to be dynamic based on features_list 6) Adding a new wrapper route that renders custom component and passes parameters 7) Swappable view on the wrapper route to configure the component 8) Button to delete/remove/disable a feature/tab 9) Migrating the existing Github/Telegram/Wiki features to utilize adjusted contract

frol commented 1 year ago

@elliotBraem Thanks for putting it all together. The plan looks solid. The only thing that I don't understand is the role of schema per CommunityFeature as I would argue it is a property of the extension component to provide its schema (I would ignore this for now, so I would just move schema field completely).

I would also suggest to migrate all the current hard-coded fields into a list of CommunityFeature.

ori-near commented 1 year ago

@elliotBraem / @petersalomonsen (and everyone else) – thank you all for all the great discussions.

A couple of updates from my side:

  1. User Story: I renamed this ticket and added a more clear product-focused user story, including your previous technical notes. Please review and let me know if we need to align on anything.
  2. Epic: I also created a new epic (using milestone feature) called: 🔷Epic: Community Plugins MVP and added this as the first foundational ticket for that scope.
elliotBraem commented 1 year ago

Discussed implementation with @Tguntenaar, taking into consideration the concerns and recommendations from @frol and @petersalomonsen.

Moved the CommunityFeature into a lookup map as recommended, to isolate feature-specific fields like src, schema, title, description, icon, etc. We need to keep the schema associated with the widget in order to create the configurator -- otherwise we can provide a custom configurator as is the case for Github and Telegram Widgets.

Here is a sketch of the overview:

image

Thomas will be implementing the contract changes and then I will be applying the front end changes. We can keep this as M

petersalomonsen commented 1 year ago

Looking good @elliotBraem @Tguntenaar !

I see the point of schema, to be able to use the components.organism.configurator widget, it needs a schema as input.

Would only add though, that this "plugin management framework" would be slightly cleaner if it wasn't there. For example you could rather create a custom_configurator widget just like the wiki configurator you showed above:

const CommunityWikiPageSchema = {
  name: {
    label: "Name",
    order: 1,
  },

  content_markdown: {
    format: "markdown",
    label: "Content",
    multiline: true,
    order: 2,
  },
};

{widget("components.organism.configurator", {
  heading: "Wiki page 2",
  data: state.communityData?.wiki2,
  isSubform: true,
  isUnlocked: permissions.can_configure,
  onSubmit: (value) => sectionSubmit({ wiki2: value }),
  submitLabel: "Accept",
  schema: CommunityWikiPageSchema,
})}

and so the framework would not have to include this logic. it would be a decision of the custom configurator to reuse a dynamic form component like the components.organism.configurator.

I believe a bit simpler and cleaner, less to maintain, and also faster to implement, but just my opinion.

But otherwise I think it's looking good!

elliotBraem commented 1 year ago

@petersalomonsen @Tguntenaar Yeah I think that makes sense. More modular as well.

Then following the custom configurator approach that @carina-akaia has already implemented:

{widget("entity.community.branding-configurator", {
  isUnlocked: permissions.can_configure,
  link,
  onSubmit: sectionSubmit,
  values: state.communityData,
})}

I think we should maintain that a configurator receives an onSubmit function and values object in props.

image

Tguntenaar commented 1 year ago

@frol @ori-near I've put this ticket in Review status for PR 61. I know the front end isn't ready yet, but we need the contract to be reviewed and merged in order to continue on the rest of the EPIC.

petersalomonsen commented 1 year ago

Is the contract deployed to another account? I guess it should be possible to review both frontend and contract together if deploying to a separate account. Probably better than having to introduce additional contract changes if you see that this is needed when working on the frontend.

frol commented 1 year ago

@Tguntenaar I have reviewed the contract code and requested changes.