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.
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:
Required | Type | Description | |
---|---|---|---|
paths | Required | array includes string or undefined | Path to determine the URL of the generated page. |
component | Required | Astro component | Content of the generated page. |
props | Object | Object 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"]
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.
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'
const getAdminPaths = async () => [
{
component: MyAstroComponent,
paths: [],
},
]
export default {
getAdminPaths,
meta: {
/*...*/
},
}
---
import {MyReactComponent} from './MyReactComponent'
---
<MyReactComponent client:load />
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
.
import { default as MyAstroComponent } from './components/MyAstroComponent.astro'
const getAdminPaths = async () => [
{
component: MyAstroComponent,
props: {
str: 'me',
},
paths: [],
},
]
export default {
getAdminPaths,
meta: {
/*...*/
},
}
---
import {MyReactComponent} from './MyReactComponent'
const { str } = Astro.props as {str: string}
---
<MyReactComponent client:load str={str} />
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.
clubs.currentPluginIndex
comes in handy when trying to access a particular plugin or updating a plugin.
---
import CurrentPluginIndexDemoComponent from './CurrentPluginIndexDemoComponent.jsx'
const { clubs } = Astro.props as ClubsPropsAdminPages
---
<CurrentPluginIndexDemoComponent client:load currentPluginIndex={clubs.currentPluginIndex} />
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.
clubs.encodedClubsConfiguration
is encoded, to obtain the configuration you will have to decode it using the helper decode
from the library clubs-core
.
---
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} />
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.
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
.
---
import CurrentPluginIndexDemoComponent from './CurrentPluginIndexDemoComponent.jsx'
const { clubs } = Astro.props as ClubsPropsAdminPages
const plugins = [...new Set(clubs.plugins)]
---
<CurrentPluginIndexDemoComponent client:load plugins={plugins} />
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.
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.
---
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.
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
.
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>
</>
)
}
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>
</>
)
}
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>
</>
)
}
The details of the plugin options are found on ClubsPluginOptions.
How to update the configuration
// 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.
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: {
/*...*/
},
}
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.
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: {
/*...*/
},
}
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
.
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: {
/*...*/
},
}
The details of the utils are found on utils.