GithubHelp home page GithubHelp logo

flaque / phelia Goto Github PK

View Code? Open in Web Editor NEW

This project forked from maxchehab/phelia

0.0 2.0 0.0 26.25 MB

⚡ A reactive Slack application framework.

Home Page: https://npmjs.com/phelia

TypeScript 99.82% JavaScript 0.18%

phelia's Introduction

⚡ Phelia

Phelia is a framework that lets you build interactive Slack applications without the webhook and JSON headache. Traditionally, building an interactive Slack application required that you designed an interface with Slack's Block Kit JSON schema. When you received one of the many interaction webhook payloads you would update the message with a new JSON schema. This leads to code fragmentation and slow development.

This framework was created to help you intuitively create Slack applications without needing to know how underlying storage, state, webhooks, or message publishing works. All you gotta know is React :).

Table of Contents

Quick start

  1. Create an express server:

    import express from "express";
    import Phelia from "phelia";
    
    import RandomImage from "./random-image";
    
    const app = express();
    
    const client = new Phelia(process.env.SLACK_TOKEN);
    
    // Set up your interaction webhook hook
    app.post(
      "/interactions",
      client.messageHandler(process.env.SLACK_SIGNING_SECRET, [RandomImage])
    );
    
    // Post a message...
    client.postMessage(RandomImage, "@max");
    
    app.listen(3000);
  2. Create your message with React:

    import randomImage from "../utils";
    
    export function RandomImage({ useState }: PheliaMessageProps) {
      const [imageUrl, setImageUrl] = useState("imageUrl", randomImage());
    
      return (
        <Message text="Choose a dog">
          <ImageBlock
            title="an adorable :dog:"
            alt="a very adorable doggy dog"
            imageUrl={imageUrl}
            emoji
          />
          <Divider />
          <Actions>
            <Button
              style="primary"
              action="randomImage"
              onClick={() => setImageUrl(randomImage())}
            >
              New doggy
            </Button>
          </Actions>
        </Message>
      );
    }
  3. Interact with your message:

How this works

Phelia transforms React components into Slack messages by use of a custom React reconciler. Components (with their internal state and props) are serialized into a custom storage. When a user interacts with a posted message Phelia retrieves the component, re-hydrates it's state and props, and performs any actions which may result in a new state.

Documentation

Surface Components

A surface is anywhere an app can express itself through communication or interaction. Each registered components should return a surface component.

Message

App-published messages are dynamic yet transient spaces. They allow users to complete workflows among their Slack conversations.

Provided Properties:

Properties Type
useState a useState function
useModal a useModal function
props a JSON serializable object

Component Properties:

Properties Type Required
children array of Actions, Context, Divider, ImageBlock, or Section components yes
text string no

Example:

const imageUrls = [
  "https://cdn.pixabay.com/photo/2015/06/08/15/02/pug-801826__480.jpg",
  "https://cdn.pixabay.com/photo/2015/03/26/09/54/pug-690566__480.jpg",
  "https://cdn.pixabay.com/photo/2018/03/31/06/31/dog-3277416__480.jpg",
  "https://cdn.pixabay.com/photo/2016/02/26/16/32/dog-1224267__480.jpg"
];

function randomImage(): string {
  const index = Math.floor(Math.random() * imageUrls.length);
  return imageUrls[index];
}

export function RandomImage({ useState }: PheliaMessageProps) {
  const [imageUrl, setImageUrl] = useState("imageUrl", randomImage());

  return (
    <Message text="Choose a dog">
      <ImageBlock
        emoji
        title={"an adorable :dog:"}
        alt={"a very adorable doggy dog"}
        imageUrl={imageUrl}
      />
      <Divider />
      <Actions>
        <Button
          style="primary"
          action="randomImage"
          onClick={() => setImageUrl(randomImage())}
          confirm={
            <Confirm
              title={"Are you sure?"}
              confirm={"Yes, gimmey that doggy!"}
              deny={"No, I hate doggies"}
            >
              <Text type="mrkdwn">
                Are you certain you want to see the _cutest_ doggy ever?
              </Text>
            </Confirm>
          }
        >
          New doggy
        </Button>
      </Actions>
    </Message>
  );
}

Modal

Modals provide focused spaces ideal for requesting and collecting data from users, or temporarily displaying dynamic and interactive information.

Provided Properties:

Properties Type
useState a useState function
props a JSON serializable object

Component Properties:

Properties Type Required
children array of Actions, Context, Divider, ImageBlock, Input, or Section components yes
title string or Text yes
submit string or Text no
close string or Text no

Example:

export function MyModal({ useState }: PheliaModalProps) {
  const [showForm, setShowForm] = useState("showForm", false);

  return (
    <Modal title="A fancy pants modal" submit="submit the form">
      {!showForm && (
        <Actions>
          <Button action="showForm" onClick={() => setShowForm(true)}>
            Show form
          </Button>
        </Actions>
      )}

      {showForm && (
        <>
          <Input label="Expiration date">
            <DatePicker action="date" />
          </Input>

          <Input label="Little bit">
            <TextField action="little-bit" placeholder="just a little bit" />
          </Input>

          <Input label="Some checkboxes">
            <Checkboxes action="checkboxes">
              <Option value="option-a">option a</Option>

              <Option value="option-b" selected>
                option b
              </Option>

              <Option value="option-c">option c</Option>
            </Checkboxes>
          </Input>

          <Input label="Summary">
            <TextField
              action="summary"
              placeholder="type something here"
              multiline
            />
          </Input>
        </>
      )}
    </Modal>
  );
}

Home

The Home tab is a persistent, yet dynamic interface for apps that lives within the App Home.

Provided Properties:

Properties Type
useState a useState function
useModal a useModal function
user *a user object

*if scope users:read is not available, the user object will only contain an id property.

Component Properties:

Properties Type Required
children array of Actions, Context, Divider, ImageBlock, or Section components yes
title string or Text no

Example:

export function HomeApp({ useState, useModal, user }: PheliaHomeProps) {
  const [counter, setCounter] = useState("counter", 0);
  const [form, setForm] = useState("form");

  const openModal = useModal("modal", MyModal, event =>
    setForm(JSON.stringify(event.form, null, 2))
  );

  return (
    <Home>
      <Section>
        <Text emoji>Hey there {user.username} :wave:</Text>
        <Text type="mrkdwn">*Counter:* {counter}</Text>
      </Section>

      <Actions>
        <Button action="counter" onClick={() => setCounter(counter + 1)}>
          Click me
        </Button>

        <Button action="modal" onClick={() => openModal()}>
          Open a Modal
        </Button>
      </Actions>

      {form && (
        <Section>
          <Text type="mrkdwn">{"```\n" + form + "\n```"}</Text>
        </Section>
      )}
    </Home>
  );
}

Custom Storage

Phelia uses a custom storage object to store posted messages and their properties such as state, props, and Component type. The persistance method can be customized by use of the client.setStorage(storage) method.

A storage object must implement the following methods:

  • set(key: string, value: string): void
  • get(key: string): string

Storage methods may be asynchronous.

By default the storage object is an in-memory map. Here is an example using Redis for storage:

import redis from "redis";
import { setStorage } from "phelia/core";

const client = redis.createClient();

setStorage({
  set: (key, value) =>
    new Promise((resolve, reject) =>
      client.set(key, value, err => (err ? reject(err) : resolve()))
    ),

  get: key =>
    new Promise((resolve, reject) =>
      client.get(key, (err, reply) => (err ? reject(err) : resolve(reply)))
    )
});

Interactive Webhooks

In order for Phelia to update your Messages or Modals you must register all of your components and setup up an interactive webhook endpoint.

Registering Components

Use the client.registerComponents method to register your components. You may pass in an array of components:

const client = new Phelia(process.env.SLACK_TOKEN);
client.registerComponents([MyModal, MyMessage]);

Pass a function which returns an array of components:

const client = new Phelia(process.env.SLACK_TOKEN);
client.registerComponents(() => [MyModal, MyMessage]);

Or pass in a directory which contains all of your components:

import path from "path";

const client = new Phelia(process.env.SLACK_TOKEN);
client.registerComponents(path.join(__dirname, "components"));

Using the messageHandler to handle interactive webhook payloads

Set a Request URL and Options Load URL in the Interactivity & Shortcuts page of your Slack application. You may need to use a reverse proxy like ngrok for local development.

Then use client.messageHandler() to intercept these webhook payloads.

const client = new Phelia(process.env.SLACK_TOKEN);

app.post(
  "/interactions",
  client.messageHandler(process.env.SLACK_SIGNING_SECRET)
);

Registering a Home Tab component

To use a Home Tab component, register a webhook for Slacks Events API and register your Home Tab component with Phelia.

Make sure that you have selected the app_home_opened bot event in the Event Subscriptions of your Slack application.

Then use client.appHomeHandler() to intercept this webhook payload.

import { createEventAdapter } from "@slack/events-api";

const slackEvents = createEventAdapter(process.env.SLACK_SIGNING_SECRET);
const client = new Phelia(process.env.SLACK_TOKEN);

slackEvents.on("app_home_opened", client.appHomeHandler(HomeApp));

app.use("/events", slackEvents.requestListener());

This requires use of Slack's SDK @slack/events-api

With this setup, whenever a user opens the Home tab it will display your Home App accordingly.

Injected Properties

Depending on which type of component you are building, Phelia will inject a collection of functions and properties into your components function.

useState Function

The useState function is very similar to it's React predecessor. Given a unique key, useState will return a pair of values; the current state and a function to modify the state. The useState function also takes an optional second parameter to specify an initial value.

Example:

function Counter({ useState }) {
  const [counter, setCounter] = useState("unique-key", 0);

  return (
    <Message>
      <Section>
        <Text type="mrkdwn">*Counter:* {counter}</Text>
      </Section>
      <Actions>
        <Button action="inc" onClick={() => setCounter(counter + 1)}>
          Increment
        </Button>
      </Actions>
    </Message>
  );
}

useModal Function

The useModal function returns a function to open a modal. Parameters include:

  1. a unique key
  2. the modal component
  3. a ModalSubmittedCallback (executed when a modal is submitted)
  4. an InteractionCallback (executed when a modal is canceled)

The function returned can be used to open a modal from within any Interaction Callback. The returned function takes an props parameter. When included, the props will be injected into the modal component.

Example:

function ModalExample({ useModal }) {
  const openModal = useModal(
    "modal",
    MyModal,
    event => console.log(event.form),
    () => console.log("canceled")
  );

  return (
    <Message text="A modal example">
      <Actions>
        <Button
          style="primary"
          action="openModal"
          onClick={event => openModal({ user: event.user })}
        >
          Open the modal
        </Button>
      </Actions>
    </Message>
  );
}

props Property

The props is a JSON serializable property injected into either Modal or Message components. props can be optional passed to either component by their respective constructors. As described above, if when opening a modal and an optional property is provided it will be passed along to the Modal component. Alternatively when using the client.postMessage function if a property is provided, it too will be passed along to the Message component.

Example:

function PropsExample({ props }) {
  return (
    <Message text="A prop example">
      <Section>
        <Text emoji>Hello {props.name} :greet:</Text>
      </Section>
    </Message>
  );
}

client.postMessage(PropsExample, "@channel", { name: "Phelia" });

user Property

The user Property is injected into Home components. It describes the user who is viewing the Home Tab taking the form of:

{
  id: string;
  username: string;
  name: string;
  team_id: string;
}

If the scope users:read is not available, only the id property will be injected.

Callback Functions

There are various different types of callback functions but all help you respond to a User interacting with your component. Each callback responds with an event object. All callback functions can be asynchronous or return a Promise.

Interaction Callback

An interaction callback is the simplest type of callback. It's event object takes the form of:

user: {
  id: string;
  username: string;
  name: string;
  team_id: string;
}

ModalSubmitted Callback

When a user submits a modal, the ModalSubmittedCallback will be called with the following event object:

form: {
  [action: string]: any
}
user: {
  id: string;
  username: string;
  name: string;
  team_id: string;
}

The event.form property is a map representing each Input child's action and value. For example the following modal:

function Modal() {
  return (
    <Modal title="A fancy pants modal" submit="submit the form">
      <Input label="Expiration date">
        <DatePicker action="date" />
      </Input>

      <Input label="Little bit">
        <TextField action="little-bit" placeholder="just a little bit" />
      </Input>

      <Input label="Some checkboxes">
        <Checkboxes action="checkboxes">
          <Option value="option-a">option a</Option>
          <Option value="option-b" selected>
            option b
          </Option>
          <Option value="option-c">option c</Option>
        </Checkboxes>
      </Input>

      <Input label="Summary">
        <TextField
          action="summary"
          placeholder="type something here"
          multiline
        />
      </Input>
    </Modal>
  );
}

would create the following event.form object:

{
  "date": "2020-4-20",
  "little-bit": "something the users typed"
  "checkboxes": ["option-a"],
  "summary": "another thing the users typed"
}

SearchOptions Callback

A SearchOptions Callback is invoked when a User types a query within a MultiSelectMenu or SelectMenu component. It must return either an array of Options or OptionGroups. It's event object takes the form of:

query: string;
user: {
  id: string;
  username: string;
  name: string;
  team_id: string;
}

SelectDate Callback

Used when a User selects a DatePicker. The event object takes the form of:

date: string;
user: {
  id: string;
  username: string;
  name: string;
  team_id: string;
}

SelectOption Callback

Used when a User selects a single option. The event object takes the form of:

selected: string;
user: {
  id: string;
  username: string;
  name: string;
  team_id: string;
}

SelectOptions Callback

Used when a User selects multiple options. The event object takes the form of:

selected: string[];
user: {
  id: string;
  username: string;
  name: string;
  team_id: string;
}

Block Components

Blocks are a series of components that can be combined to create visually rich and compellingly interactive messages.

Actions

A block that is used to hold interactive elements.

Component Properties:

Properties Type Required
children array of Button, SelectMenu, RadioButtons, MultiSelectMenu, Checkboxes, OverflowMenu, or DatePicker components yes

Example:

<Actions>
  <Button action="showForm" onClick={event => setShowForm(true)}>
    Show form
  </Button>

  <DatePicker onSelect={event => setDate(event.selected)} action="date" />
</Actions>

Context

Displays message context, which can include both images and text.

Component Properties:

Properties Type Required
children array of Image or Text components yes

Example:

<Context>
  <ImageBlock imageUrl="https://google.com/image.png" alt="an image" />
</Context>

Divider

A content divider, like an <hr>, to split up different blocks inside of a surface. It does not have any properties.

Example:

<Divider />

ImageBlock

A simple image block.

Component Properties:

Properties Type Required
alt string yes
emoji boolean no
imageUrl string yes
title string no

Example:

<ImageBlock imageUrl="https://google.com/image.png" alt="an image" />

Input

A block that collects information from users

Component Properties:

Properties Type Required
children a TextField, SelectMenu, MultiSelectMenu, or DatePicker component yes
hint string or Text no
label string or Text yes
optional boolean no

Example:

<Input label="Expiration date">
  <DatePicker action="date" />
</Input>

Section

A block that collects information from users

Component Properties:

Properties Type Required
accessory a Button, SelectMenu, RadioButtons, MultiSelectMenu, Checkboxes, OverflowMenu, or DatePicker component no
children an array of Text components if the text property is not included
text string or Text if no children are included

Example:

<Section
  text={"Select your birthday."}
  accessory={
    <DatePicker
      onSelect={async ({ user, date }) => {
        setBirth(date);
        setUser(user.username);
      }}
      action="date"
    />
  }
/>

Block Elements

Button

An interactive component that inserts a button. The button can be a trigger for anything from opening a simple link to starting a complex workflow.

Component Properties:

Properties Type Required
action string if an onClick property is provided
children string yes
confirm Confirm no
onClick InteractionCallback no
style "danger" or "primary" no
url string no

Example:

<Button action="name" onClick={event => setName(event.user.username)}>
  Set name
</Button>
<Button url="https://google.com">Open google</Button>

Checkboxes

A checkbox group that allows a user to choose multiple items from a list of possible options.

Component Properties:

Properties Type Required
action string yes
children array of Option components yes
confirm Confirm no
onSelect SelectOptionsCallback no

Example:

<Checkboxes action="options" onSelect={event => setSelected(event.selected)}>
  <Option value="1" selected>
    I am initially selected
  </Option>
  <Option value="2">hello</Option>
</Checkboxes>

DatePicker

An element which lets users easily select a date from a calendar style UI

Component Properties:

Properties Type Required
action string yes
confirm Confirm no
initialDate string no
onSelect SelectDateCallback no
placeholder string or Text no

Example:

<DatePicker
  onSelect={event => setDate(event.date)}
  action="date"
  initialDate="2020-11-11"
/>

Image

An element to insert an image as part of a larger block of content. If you want a block with only an image in it, you're looking for the Image Block.

Component Properties:

Properties Type Required
imageUrl string yes
alt string yes

Example:

<Image imageUrl="https://images.com/dog.png" alt="an image of a dog" />

MultiSelectMenu

A multi-select menu allows a user to select multiple items from a list of options.

Component Properties:

Properties Type Required
action string yes
placeholder string or Text yes
confirm Confirm no
onSelect SelectOptionsCallback no
maxSelectedItems integer no
type "static" "users" "channels" "external" or "conversations" no
children array of Option or OptionGroup components if "static" type
initialUsers array of User Ids if "users" type
initialChannels array of Channel Ids if "channels" type
initialOptions array of Option components if "external" type
onSearchOptions a SearchOptionsCallback if "external" type
minQueryLength integer if "external" type
initialConversations array of Conversation Ids if "conversations" type
filter a ConversationFilter object if "conversations" type

Examples:

<MultiSelectMenu
  onSearchOptions={event => filterUsers(event.query)}
  type="external"
  action="select-users"
  placeholder="Select a user"
/>
<MultiSelectMenu action="selection" placeholder="Select an option">
  <OptionGroup label="an option group">
    <Option value="option-a">option a</Option>
    <Option value="option-b">option b</Option>
    <Option value="option-c">option c</Option>
  </OptionGroup>

  <OptionGroup label="another option group">
    <Option value="option-d">option d</Option>
    <Option value="option-e" selected>
      option e
    </Option>
    <Option value="option-f">option f</Option>
  </OptionGroup>
</MultiSelectMenu>

OverflowMenu

Presents a list of options to choose from with no type-ahead field, and the button always appears with an ellipsis ("…") rather than a placeholder.

Component Properties:

Properties Type Required
action string yes
children array of Option or OptionGroup components yes
confirm Confirm no
onSelect SelectOptionCallback no

Example:

<OverflowMenu action="overflow" onSelect={event => setSelected(event.selected)}>
  <Option value="dogs">Dogs</Option>
  <Option value="cats">Cats</Option>
  <Option url="https://pixabay.com/images/search/dog/" value="a-link">
    Dog images
  </Option>
</OverflowMenu>

RadioButtons

A radio button group that allows a user to choose one item from a list of possible options.

Component Properties:

Properties Type Required
action string yes
children array of Option or OptionGroup components yes
confirm Confirm no
onSelect SelectOptionCallback no

Example:

<RadioButtons
  action="radio-buttons"
  onSelect={event => setSelected(event.selected)}
>
  <Option value="option-a">option a</Option>
  <Option value="option-b" selected>
    option b
  </Option>
  <Option value="option-c">option c</Option>
</RadioButtons>

SelectMenu

A select menu creates a drop down menu with a list of options for a user to choose. The select menu also includes type-ahead functionality, where a user can type a part or all of an option string to filter the list.

Component Properties:

Properties Type Required
action string yes
placeholder string or Text yes
confirm Confirm no
onSelect SelectOptionCallback no
type "static" "users" "channels" "external" or "conversations" no
children an array of Option or OptionGroup components if "static" type
initialUsers User Ids if "users" type
initialChannel Channel Ids if "channels" type
initialOption Option if "external" type
onSearchOptions a SearchOptionsCallback if "external" type
minQueryLength integer if "external" type
initialConversation Conversation Ids if "conversations" type
filter a ConversationFilter object if "conversations" type

Examples:

<SelectMenu
  onSearchOptions={event => filterUsers(event.query)}
  type="external"
  action="select-menu"
  placeholder="Select a user"
/>
<SelectMenu
  type="users"
  action="select-groups"
  placeholder="Select a user"
  onSelect={event => setSelected(event.selected)}
/>

TextField

A plain-text input creates a field where a user can enter freeform data. It can appear as a single-line field or a larger textarea using the multiline flag.

Component Properties:

Properties Type Required
action string yes
initialValue string no
maxLength integer no
minLength integer no
multiline boolean no
placeholder string or Text no

Example:

<TextField action="summary" placeholder="type something here" multiline />

Composition Elements

Composition Elements are commonly used elements.

Text

An element with text.

Component Properties:

Properties Type Required
children string yes
emoji boolean if "plain_text" type
type "plain_text" or "mrkdwn" no
verbatim boolean if "mrkdwn" type

Example:

<Text emoji>Hello there :wave:</Text>

Confirm

An object that defines a dialog that provides a confirmation step to any interactive element. This dialog will ask the user to confirm their action by offering a confirm and deny buttons.

Component Properties:

Properties Type Required
children string or Text yes
confirm string or Text yes
deny string or Text yes
style "danger" or "primary" no
title string or Text yes

Example:

<Button
  emoji
  confirm={
    <Confirm title="Confirm me?" confirm="Yes, I confirm!" deny="No, go away!">
      Do you confirm me?
    </Confirm>
  }
>
  Click me
</Button>

Option

Represents a single selectable item in a SelectMenu, MultiSelectMenu, Checkboxes, RadioButtons, or OverflowMenu.

Component Properties:

Properties Type Required
children string or Text yes
description string or Text no
selected boolean no
url string no
value string no

Example:

<Option value="option-1" selected>
  An option
</Option>

OptionGroup

Provides a way to group options in a SelectMenu or MultiSelectMenu

Component Properties:

Properties Type Required
children array of Option components yes
label string or Text yes

Example:

<OptionGroup label="an option group">
  <Option value="option-a">option a</Option>
  <Option value="option-b">option b</Option>
  <Option value="option-c">option c</Option>
</OptionGroup>

ConversationFilter

Provides a way to filter the list of Conversations in a SelectMenu or MultiSelectMenu

Properties Type Required
include "im" "mpim" "private" "public" no
excludeBotUsers boolean no
excludeExternalSharedChannels boolean no

Example:

<MultiSelectMenu
  type="conversations"
  action="select-conversation"
  filter={{
    include: "im",
    excludeBotUsers: true,
    excludeExternalSharedChannels: true
  }}
/>

Feature Support

To request a feature submit a new issue.

Component Example
Actions Counter
Button Counter
Channel Select Menus Channel Select Menu
Checkboxes Modal Example
Confirmation dialog Random Image
Context
Conversation Select Menus Conversation Select Menu
Date Picker Birthday Picker
Divider Random Image
External Select Menus External Select Menu
Home Tab Home App Example
Image Block Random Image
Image Random Image
Input Modal Example
Messages Server
Modals Modal Example
Multi channels select Menu Multi Channels Select Menu
Multi conversations select Menu Multi Conversations Select Menu
Multi external select Menu Multi External Select Menu
Multi static select Menu Multi Static Select Menu
Multi users select Menu Multi Users Select Menu
Option group Static Select Menu
Option
Overflow Menu Overflow Menu
Plain-text input Modal Example
Radio button group Radio Buttons
Section Counter
Static Select Menus Static Select Menu
Text Counter
Text Random Image
User Select Menus User Select Menu

phelia's People

Contributors

maxchehab avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.