メインコンテンツまでスキップ

Admin pages

Building an interactive plugin that is configurable by the user can be implemented by adding admin pages.

By default, Clubs has some built-in admin pages starting with /admin; you can use getAdminPaths to add plugin's own pages to the admin area.

note

The storing method for the configuration is dependent on an individual Clubs environment, and so the plugin can be indifferent to the storing method. See the setOptions section for more information.

getAdminPaths

getAdminPaths is an asynchronous function that generates admin pages and returns an array of ClubsStaticPaths to define pages and their contents, etc.

Clubs runtime passes 3 arguments to getAdminPaths, plugin options, config, and utils. And the return value should be an array of objects with the following values:

RequiredTypeDescription
pathsRequiredarray includes string, RegExp or undefinedPath to determine the URL of the generated page.
componentRequiredAstro componentContent of the generated page.
propsObjectObject passed to Astro.props

The returns value

The return value of getAdminPaths should be an array of objects with these properties.

paths

If paths joined with / matches the pathname in the HTML request without /admin, the component will be rendered as the page content.

For example, if you want to add /admin/vote/add, paths should be the following value:

["vote", "add"]

In getAdminPaths, /admin is always added as a prefix.

Note that the following paths are reserved by the built-in admin pages and will not work even if you specify them

[]
[undefined]
["overview"]
["theme"]
info

// TODO: Add a note about RegExp path

component

Components must always be Astro components. If you want to use a UI framework such as React or Svelte, for example, you can export an Astro component that renders a React component.

/src/index.ts
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'

const getAdminPaths = async () => [
{
component: MyAstroComponent,
paths: [],
},
]

export default {
getAdminPaths,
meta: {
/*...*/
},
}
/src/components/MyAstroComponent.astro
---
import {MyReactComponent} from './MyReactComponent'
---

<MyReactComponent client:load />
/src/components/MyReactComponent.tsx
import React from 'react'

export const MyReactComponent = () => <p>Hello, world!</p>

props

If a component has values that it expects to be injected from the outside, you can pass those values through defining props.

/src/index.ts
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'

const getAdminPaths = async () => [
{
component: MyAstroComponent,
props: {
str: 'me',
},
paths: [],
},
]

export default {
getAdminPaths,
meta: {
/*...*/
},
}
/src/components/MyAstroComponent.astro
---
import {MyReactComponent} from './MyReactComponent'

const { str } = Astro.props as {str: string}
---

<MyReactComponent client:load str={str} />
/src/components/MyReactComponent.tsx
import React from 'react'

export const MyReactComponent = ({ str }: { str: string }) => (
<p>Hello, {str}!</p>
)

Special props for admin pages

The special props, clubs is automatically appended to Astro.props in all admin page components.

The type of clubs is included in ClubsPropsAdminPages and it is defined by clubs-core.

clubs.currentPluginIndex

The prop clubs.currentPluginIndex is injected in all admin page components by the core librray clubs-core. It represents the index of a plugin in it's club configuration.

info

clubs.currentPluginIndex comes in handy when trying to access a particular plugin or updating a plugin.

.../CurrentPluginIndexDemo.astro
---

import CurrentPluginIndexDemoComponent from './CurrentPluginIndexDemoComponent.jsx'

const { clubs } = Astro.props as ClubsPropsAdminPages
---

<CurrentPluginIndexDemoComponent client:load currentPluginIndex={clubs.currentPluginIndex} />
.../CurrentPluginIndexDemoComponent.jsx
export default (props) => {
console.log('Props received to the component', { props })

const updatePluginOptions = () => {
// Triggers the plugin options update directly for a plugin of a club.
setOptions([{ key: 'k', value: 1 }], props.currentPluginIndex)
}

return (
<>
<p>Updating plugin options example:</p>
<button className="hs-button is-large" onClick={updatePluginOptions}>
Update plugin option
</button>
</>
)
}

clubs.encodedClubsConfiguration

The prop clubs.encodedClubsConfiguration holds the entire club configuration encoded. It can be used to access the club information within the component or any child components.

note

clubs.encodedClubsConfiguration is encoded, to obtain the configuration you will have to decode it using the helper decode from the library clubs-core.

.../EncodedClubsConfigurationDemo.astro
---

import { decode, encode } from '@devprotocol/clubs-core'

import CurrentPluginIndexDemoComponent from './CurrentPluginIndexDemoComponent.jsx'

const { clubs } = Astro.props as ClubsPropsAdminPages
const plugins = decode(clubs.encodedClubsConfiguration).plugins

---

<CurrentPluginIndexDemoComponent client:load plugins={plugins} />

.../EncodedClubsConfigurationDemoComponent.jsx
import React from 'react'

export default (props) => {
console.log('Props received to the component', { props })

return (
<>
<p>All plugins are:</p>
{props.plugins.map((plugin) => (
<p>
The plugin {plugin.id} is {plugin.enabled ? 'Enabled' : 'Disabled'}
</p>
))}
</>
)
}

clubs.plugins

The prop clubs.plugins gives the enabled plugins used by a club.

note

clubs.plugins returns all the plugins which are currently enabled. If there is a need to get all the installed plugins for a club we need get them by decoding the clubs.encodedClubsConfiguration.

.../PluginsDemo.astro
---

import CurrentPluginIndexDemoComponent from './CurrentPluginIndexDemoComponent.jsx'

const { clubs } = Astro.props as ClubsPropsAdminPages
const plugins = [...new Set(clubs.plugins)]

---

<CurrentPluginIndexDemoComponent client:load plugins={plugins} />

.../PluginsDemoComponent.jsx
import React from 'react'

export default (props) => {
console.log('Props received to the component', { props })

return (
<>
<p>All enabled plugins are:</p>
{props.plugins.map((plugin) => (
<p>The plugin {plugin.id}</p>
))}
</>
)
}

clubs.slots

The prop clubs.slots gives the components specified by the plugin developers that are to be injected into the UI slots.

info

There can be multiple components in clubs.slots each pointing to a different slot left in the UI or each pointing to the same slot.

.../SlotsDemo.astro
---

import CurrentPluginIndexDemoComponent from './CurrentPluginIndexDemoComponent.jsx'

const { clubs } = Astro.props as ClubsPropsAdminPages

const slotsForPositionA = clubs.slots.filter(
(slot) => slot.slot === 'position:a:slot'
)

const slotsForPositionB = clubs.slots.filter(
(slot) => slot.slot === 'position:b:slot'
)
---

<div>
<p>All slots for position A</p>
{slotsForPositionA.map((Slot) => (
<Slot.component {...Slot.props} />
))}

<br />

<p>All slots for position B</p>
{slotsForPositionB.map((Slot) => (
<Slot.component {...Slot.props} />
))}
</div>

How to update the plugin options

The library clubs-core has helper and utility functions that can be used to update a club easily and efficiently. Plugin options are designed to store any and all necessary information about a club's plugin and has the type ClubsPluginOptions(*).

The helper functions setOptions and buildConfig enables updating plugin options. Using setOptions will trigger the DOM(browser managed local instance of a club) to update plugin options locally in the browser's memory. Using buildConfig will trigger the update from DOM to the database making the local update permanent.

In order to update the plugin option persistently, one has to use both setOptions and buildConfig. First use setOptions with the update information to make the chances locally on the browser and then use buildConfig to make those local non-persistent updates permanent.

The setOptions helper accepts two paramters: the new plugin options and your clubs configuration plugin index. The buildConfig helper accepts no parameter- it takes the current local configuration and updates it persistently.

note

The setOptions helper function updates the entire state of a plugin options.

If you have to retain any or all portion of the plugin option state, you will have to inject it in the new state(avoiding duplication) and then use setOptions.

.../UpdatePluginOptionsDemo1.jsx
import React from 'react'

import { buildConfig, setOptions } from '../src/events'

export default (props) => {
console.log('Props received to the component', { props })

const updatePluginOptions = () => {
// Triggers the plugin options update to local browser DOM.
setOptions([{ key: 'k', value: 1 }], props.clubs.currentPluginIndex)

// Triggers the updating of plugin options to the database.
setTimeout(buildConfig, 50)
}

return (
<>
<p>Updating plugin options example:</p>
<button className="hs-button is-large" onClick={updatePluginOptions}>
Update plugin option
</button>
</>
)
}
.../UpdatePluginOptionsDemo2.jsx
import React from 'react'

import { buildConfig, setOptions } from '../src/events'

export default (props) => {
console.log('Props received to the component', { props })

const [name, setName] = React.useEffect('')

const updatePluginOptions = (value) => {
// Triggers the plugin options update to local browser DOM.
setOptions([{ key: 'k', value }], props.clubs.currentPluginIndex)

// Triggers the updating of plugin options to the database.
setTimeout(buildConfig, 50)
}

return (
<>
<p>Updating plugin options example:</p>
<label class="hs-form-field is-filled is-required">
<span class="hs-form-field__label">Name: </span>
<input
class="hs-form-field__input"
name="input-name"
onClick={(input) => setName(input)}
/>
</label>
<button
className="hs-button is-large"
onClick={() => updatePluginOptions(name)}
>
Update plugin option
</button>
</>
)
}
.../UpdatePluginOptionsDemo3.jsx
import React from 'react'

import { buildConfig, setOptions } from '../src/events'

export default (props) => {
console.log('Props received to the component', { props })

const [name, setName] = React.useEffect('')

const updatePluginOptions = (value) => {
const oldValue =
/* `Logic to retain the a portion or entire old plugin options
while making sure to avoid information duplication` */

// Triggers the plugin options update to local browser DOM.
setOptions(
[{ key: 'k', value: [...oldValue, value] }],
props.clubs.currentPluginIndex
)

// Triggers the updating of plugin options to the database.
setTimeout(buildConfig, 50)
}

return (
<>
<p>Updating plugin options example:</p>
<label class="hs-form-field is-filled is-required">
<span class="hs-form-field__label">Name: </span>
<input
class="hs-form-field__input"
name="input-name"
onClick={(input) => setName(input)}
/>
</label>
<button
className="hs-button is-large"
onClick={() => updatePluginOptions(name)}
>
Update plugin option
</button>
</>
)
}
info

The details of the plugin options are found on ClubsPluginOptions.

How to update the configuration

info

// TODO: Write this

Use plugin options

The 1st argument of getAdminPaths is passed an object plugin options, has type ClubsPluginOptions(*). By controlling the return value of getAdminPaths with plugin options, you can control the page's functionality of the page.

/src/index.ts
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'

const getAdminPaths = async (options) => {
const str = options.find(({ key }) => key === 'content')?.value ?? 'me'

return [
{
component: MyAstroComponent,
props: {
str,
},
paths: [],
},
]
}

export default {
getAdminPaths,
meta: {
/*...*/
},
}
info

The details of the plugin options are found on ClubsPluginOptions.

Use configuration

The 2nd argument of getAdminPaths is passed an object config, has type ClubsConfiguration(*). By controlling the return value of getAdminPaths with config, you can control the page's functionality of the page.

ClubsConfiguration contains major primitive configuration values such as Clubs name, property token address, chain ID which tokenized it, URL of Json-RPC endpoint, etc.

/src/index.ts
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'

const getAdminPaths = async (options, config) => {
const str = options.find(({ key }) => key === 'content')?.value ?? 'me'
const { rpcUrl } = config

return [
{
component: MyAstroComponent,
props: {
str,
rpcUrl,
},
layout: MyLayout,
paths: [],
},
]
}

export default {
getAdminPaths,
meta: {
/*...*/
},
}
info

The details of the config are found on ClubsConfiguration.

Use utils

The 3rd argument of getAdminPaths is passed an object utils, has type ClubsFactoryUtils(*). ClubsFactoryUtils has a function getPluginConfigById that allows users to retrieve configuration values for other plugins using getPluginConfigById.

/src/index.ts
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'

const getAdminPaths = async (options, config, utils) => {
const str = options.find(({ key }) => key === 'content')?.value ?? 'me'
const { rpcUrl } = config
const emojiPlugin = utils.getPluginConfigById('emoji:plugin:id') // Find a config of a plugin that has `meta.id: "emoji:plugin:id"`
const emoji =
emojiPlugin?.options?.find(({ key }) => key === 'emoji')?.value ?? '🦄'

return [
{
component: MyAstroComponent,
props: {
str,
rpcUrl,
emoji,
},
layout: MyLayout,
paths: [],
},
]
}

export default {
getAdminPaths,
meta: {
/*...*/
},
}
info

The details of the utils are found on utils.