Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(custom-commands): Expand custom command arg with placeholder and… #1456

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 25 additions & 1 deletion apps/example-app/app/devtools/ReactotronConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ if (Platform.OS !== "web") {
})
}

type Arg<AN extends string, AT extends ArgType = ArgType.String> = {
name: AN
type: AT
placeholder?: string
hidden?: boolean
}

/**
* Reactotron allows you to define custom commands that you can run
* from Reactotron itself, and they will run in your app.
Expand Down Expand Up @@ -83,7 +90,7 @@ reactotron.onCustomCommand({
},
})

reactotron.onCustomCommand<[{ name: "route"; type: ArgType.String }]>({
reactotron.onCustomCommand<[Arg<"route">]>({
command: "navigateTo",
handler: (args) => {
const { route } = args ?? {}
Expand All @@ -109,6 +116,23 @@ reactotron.onCustomCommand({
},
})

reactotron.onCustomCommand<[Arg<"data">]>({
title: "Log hidden data",
description: "Logs hidden input data",
command: "logHiddenData",
handler: (args) => {
Reactotron.log(`Hidden data: ${args?.data}`)
},
args: [
{
name: "data",
placeholder: "Provide hidden data",
hidden: true,
type: ArgType.String,
},
],
})

/**
* We're going to add `console.tron` to the Reactotron object.
* Now, anywhere in our app in development, we can use Reactotron like so:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type { CustomCommand } from "reactotron-core-ui"
import React, { useReducer } from "react"
import {
CustomCommandItemState,
CustomCommandItemActionType,
customCommandItemReducer,
customCommandItemReducerInitializer,
} from "../reducers/customCommandItemReducer"
import styled from "styled-components"

const ButtonContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 24px;
color: ${(props) => props.theme.foreground};
`
const Title = styled.div`
font-size: 24px;
margin-bottom: 12px;
`
const Description = styled.div`
margin-bottom: 12px;
`
const ArgsContainer = styled.div`
margin-bottom: 24px;
`
const SendButton = styled.div`
display: flex;
align-items: center;
justify-content: center;
background-color: ${(props) => props.theme.backgroundLighter};
border-radius: 4px;
width: 200px;
min-height: 50px;
margin-bottom: 24px;
cursor: pointer;
color: white;
transition: background-color 0.25s ease-in-out;

&:hover {
background-color: #e73435;
}
`
const ArgContainer = styled.div`
&:not(:last-child) {
margin-bottom: 12px;
}
`
const ArgName = styled.div`
margin-bottom: 8px;
`
const ArgInput = styled.input`
padding: 10px 12px;
outline: none;
border-radius: 4px;
width: 90%;
border: none;
font-size: 16px;
`

export default function CustomCommandItem({
customCommand,
sendCustomCommand,
}: {
customCommand: CustomCommand
sendCustomCommand: (command: string, args: CustomCommandItemState) => void
}) {
const [state, dispatch] = useReducer(
customCommandItemReducer,
customCommand.args,
customCommandItemReducerInitializer
)

const handleArgInputChange = (argName: string) => {
return (event: React.ChangeEvent<HTMLInputElement>) => {
dispatch({
type: CustomCommandItemActionType.UPDATE_ARG,
payload: {
argName,
value: event.target.value,
},
})
}
}

return (
<ButtonContainer>
<Title>{customCommand.title || customCommand.command}</Title>
<Description>{customCommand.description || "No Description Provided"}</Description>
{!!customCommand.args && customCommand.args.length > 0 && (
<ArgsContainer>
{customCommand.args.map((arg) => {
const hidden = arg.hidden || false
return (
<ArgContainer key={arg.name}>
<ArgName>{arg.name}</ArgName>
<ArgInput
type={hidden ? "password" : "text"}
placeholder={arg.placeholder ?? arg.name}
value={state[arg.name]}
onChange={handleArgInputChange(arg.name)}
/>
</ArgContainer>
)
})}
</ArgsContainer>
)}
<SendButton
onClick={() => {
sendCustomCommand(customCommand.command, state)
}}
>
Send Command
</SendButton>
</ButtonContainer>
)
}
128 changes: 2 additions & 126 deletions apps/reactotron-app/src/renderer/pages/customCommands/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useState, useContext, useReducer } from "react"
import React, { useState, useContext } from "react"
import { Header, EmptyState, CustomCommandsContext } from "reactotron-core-ui"
import type { CustomCommand } from "reactotron-core-ui"
import styled from "styled-components"
import { MdSearch } from "react-icons/md"
import { FaMagic } from "react-icons/fa"
import { produce } from "immer"
import CustomCommandItem from "./components/CustomCommandItem"

const Container = styled.div`
display: flex;
Expand Down Expand Up @@ -42,129 +41,6 @@ const SearchInput = styled.input`
font-size: 14px;
`

const ButtonContianer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 24px;
color: ${(props) => props.theme.foreground};
`
const Title = styled.div`
font-size: 24px;
margin-bottom: 12px;
`
const Description = styled.div`
margin-bottom: 12px;
`
const ArgsContainer = styled.div`
margin-bottom: 24px;
`
const SendButton = styled.div`
display: flex;
align-items: center;
justify-content: center;
background-color: ${(props) => props.theme.backgroundLighter};
border-radius: 4px;
width: 200px;
min-height: 50px;
margin-bottom: 24px;
cursor: pointer;
color: white;
transition: background-color 0.25s ease-in-out;

&:hover {
background-color: #e73435;
}
`
const ArgContainer = styled.div`
&:not(:last-child) {
margin-bottom: 12px;
}
`
const ArgName = styled.div`
margin-bottom: 8px;
`
const ArgInput = styled.input`
padding: 10px 12px;
outline: none;
border-radius: 4px;
width: 90%;
border: none;
font-size: 16px;
`

// TODO: This item thing is getting complicated, move it out!
// TODO: Better typing
function customCommandItemReducer(state: any, action: any) {
switch (action.type) {
case "UPDATE_ARG":
return produce(state, (draftState) => {
draftState[action.payload.argName] = action.payload.value
})
default:
return state
}
}

function CustomCommandItem({
customCommand,
sendCustomCommand,
}: {
customCommand: CustomCommand
sendCustomCommand: (command: any, args: any) => void
}) {
const [state, dispatch] = useReducer(customCommandItemReducer, customCommand.args, (args) => {
if (!args) return {}

const argMap = {}

args.forEach((arg) => {
argMap[arg.name] = ""
})

return argMap
})

return (
<ButtonContianer>
<Title>{customCommand.title || customCommand.command}</Title>
<Description>{customCommand.description || "No Description Provided"}</Description>
{!!customCommand.args && customCommand.args.length > 0 && (
<ArgsContainer>
{customCommand.args.map((arg) => {
return (
<ArgContainer key={arg.name}>
<ArgName>{arg.name}</ArgName>
<ArgInput
type="text"
placeholder={arg.name}
value={state[arg.name]}
onChange={(e) => {
dispatch({
type: "UPDATE_ARG",
payload: {
argName: arg.name,
value: e.target.value,
},
})
}}
/>
</ArgContainer>
)
})}
</ArgsContainer>
)}
<SendButton
onClick={() => {
sendCustomCommand(customCommand.command, state)
}}
>
Send Command
</SendButton>
</ButtonContianer>
)
}

function CustomCommands() {
const [isSearchOpen, setSearchOpen] = useState(false)
const [search, setSearch] = useState("")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { produce } from "immer"
import { CustomCommandArg } from "reactotron-core-client"

export enum CustomCommandItemActionType {
UPDATE_ARG = "UPDATE_ARG",
}

export type CustomCommandItemState = {
[argName: string]: string
}
export type CustomCommandItemActionPayload = {
argName: string
value: string
}
export type CustomCommandItemAction = {
type: CustomCommandItemActionType
payload: CustomCommandItemActionPayload
}

export function customCommandItemReducer(
state: CustomCommandItemState,
action: CustomCommandItemAction
) {
switch (action.type) {
case CustomCommandItemActionType.UPDATE_ARG:
return produce(state, (draftState) => {
draftState[action.payload.argName] = action.payload.value
})
default:
return state
}
}

export function customCommandItemReducerInitializer(args: CustomCommandArg[]) {
if (!args) {
return {}
}
return args.reduce<CustomCommandItemState>((acc, arg) => {
acc[arg.name] = ""
return acc
}, {})
}
2 changes: 2 additions & 0 deletions lib/reactotron-core-client/src/reactotron-core-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export enum ArgType {
export interface CustomCommandArg {
name: string
type: ArgType
placeholder?: string
hidden?: boolean
}

// #region Plugin Types
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import type { Command } from "reactotron-core-contract"
import { CommandType } from "reactotron-core-contract"
import ReactotronContext from "../Reactotron"

export type CustomCommandArg = {
name: string
placeholder?: string
hidden?: boolean
}

export interface CustomCommand {
clientId: string
id: string
title?: string
command: string
description?: string
args?: {
name: string
}[]
args?: CustomCommandArg[]
}

interface CustomCommandState {
Expand Down