Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ This assumes you have the following technologies installed and on your path:

I recommend using something like [nvm](https://github.com/nvm-sh/nvm#installing-and-updating) for installing and managing versions of node but any method will work.

#### Quick setup (recommended)

A `setup.sh` script is provided at the root of the repository that automates the installation of all required tools and dependencies. It will install nvm, Node.js, Yarn, and all project packages:

```bash
$ bash setup.sh
```

#### Manual setup

1. Fork the repo (Once we see any semi-serious input from a developer we will grant write permissions to our central repository. You can also request this earlier if you wish.)
2. Install dependencies
Expand All @@ -33,6 +42,39 @@ SERVER=<flexget_api:port> yarn start
Then you can go to http://localhost:8000 (use `PORT` env variable to run on a different port) in your browser.
4. After you've made your changes, run `yarn lint`, `yarn test` and then open a PR.

### Running in the background

A `run_server` script is provided at the root of the repository to start the development server as a background process. It requires the URL of your running FlexGet instance as its only argument:

```bash
$ ./run_server http://<flexget_host>:<port>
```

For example:

```bash
$ ./run_server http://192.168.1.228:5050
```

Once started, the WebUI will be available at http://localhost:8000. Output from the server is written to `.webui.log` in the project root.

If the server is already running, the script will report its PID and exit without starting a second instance.

#### Stopping the server

Add the `stop-webui` shell alias to your environment by sourcing your `~/.bashrc`:

```bash
# alias stop-webui='pkill -f "babel-node.*server.js" 2>/dev/null && rm -f ~/personal/webui/.webui.pid && echo "WebUI stopped" || echo "WebUI is not running"'
$ source ~/.bashrc
```

Then stop the server at any time with:

```bash
$ stop-webui
```

### Notes
* We are in the process of moving from javascript to typescript and if you are making substantial changes to a javascript file, please convert it to typescript if you feel comfortable doing so. All new files should be written in typescript.

Expand Down
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Project-specific guidance

## Stack
React 16 + TypeScript 4, Material UI v4, Emotion CSS, Formik, Webpack 4, Yarn 1.x.
Run tests: `yarn test --no-coverage` (requires Node 16 via nvm).

## Plugin registration
New plugins live in `src/plugins/<name>/`. Register in `src/Root.tsx` by importing
and calling the default export from `src/plugins/<name>/index.ts`, which calls
`registerPlugin(path, { component, displayName, icon })`.
To hide a plugin from the sidebar without removing its route, pass `hidden: true`
to `registerPlugin`. The `SideNav` filters it out; `Routes` still registers it.

## API hooks
- `useFlexgetAPI<T>(url, method)` — REST calls; URL is fixed at hook creation time.
- `useFlexgetStream(url, method)` — oboe streaming; returns `[{ stream, readyState }, { connect, disconnect }]`.
Attach `.node()`, `.done()`, `.fail()` handlers in a `useEffect([stream])` — the hook itself only
handles `.start()` and `.fail()` for `ReadyState`. There is NO built-in `.done()` handler, so
`readyState` never returns to `Closed` on a successful stream; attach `.done()` directly on the
stream object to detect completion.
- Request bodies are auto-converted to snake_case; responses are auto-camelized.
- `useFlexgetAPI` URL is fixed per render. For DELETE/PUT calls where the path param
changes at submit time (e.g. task name edited by user), store the value in `useState`
and pass it to the hook — a re-render updates the request fn. When that request fires
after an async operation (e.g. a stream `.done()`), hold the callback in a `useRef`
so the stream effect doesn't list the callback as a dep and won't re-attach handlers
on re-render: `const ref = useRef(fn); useEffect(() => { ref.current = fn; }, [fn]);`

## MUI + Emotion css prop conflict
When extending `React.HTMLAttributes<HTMLDivElement>` for a component that renders
inside MUI/Emotion, use `Omit<React.HTMLAttributes<HTMLDivElement>, 'css'>`.
Emotion globally augments `HTMLAttributes` with its own `css` type, which conflicts
with MUI Box's `css` prop, causing a TS error at component definition time.

## Testing
- MUI v4 `TextField` without an explicit `id` prop doesn't wire `htmlFor` in JSDOM.
Use `container.querySelector('[name="fieldName"]')` instead of `getByLabelText`.
- `@testing-library/react` v9 has no `name` option on `getByRole`.
Use `getByText('Label').closest('button')` for buttons.
- `act` is not exported from `@testing-library/react` v9; import from `react-dom/test-utils`.
- To mock `useFlexgetStream` in tests, use `jest.spyOn(coreApi, 'useFlexgetStream')` —
ts-jest compiles to CommonJS so named-import spying works.
- Async tests that involve navigation → API fetch → Formik reinitialize need extended
timeouts; set `jest.setTimeout(15000)` in `beforeAll`.
- MUI v4 `Select` doesn't wire `[data-value]` reliably in JSDOM. Open with
`fireEvent.mouseDown(selectEl)`, then find options via
`document.querySelectorAll('[role="option"]')` (they render into a portal) and
match by `el.textContent?.trim()`.
- When multiple `Select` components are on the page, `.MuiSelect-root` indices follow
JSX render order, not visual position. Confirm the index of the target Select before
using it in a test.
- When `fetchMock` returns a non-ok status the error object carries a `message` string
(the HTTP status text), so a `?? 'Unknown error'` fallback is never reached that way.
To test the unknown-error path, mock the hook directly:
`jest.spyOn(hooks, 'useCreate...').mockReturnValue([..., jest.fn().mockResolvedValue({ ok: false, error: undefined })])`
35 changes: 35 additions & 0 deletions run_server.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PID_FILE="$SCRIPT_DIR/.webui.pid"
LOG_FILE="$SCRIPT_DIR/.webui.log"

if [ -z "${1:-}" ]; then
echo "Usage: run_server <flexget_server_url>"
echo " e.g. run_server http://192.168.1.228:5050"
exit 1
fi

FLEXGET_SERVER="$1"

if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
echo "WebUI is already running (PID $(cat "$PID_FILE"))"
echo " Log: $LOG_FILE"
echo " Stop: stop-webui"
exit 0
fi

export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"

cd "$SCRIPT_DIR"

SERVER="$FLEXGET_SERVER" yarn start >"$LOG_FILE" 2>&1 &
echo $! >"$PID_FILE"

echo "WebUI started (PID $!)"
echo " URL: http://localhost:8000"
echo " API: $FLEXGET_SERVER"
echo " Log: $LOG_FILE"
echo " Stop: stop-webui"
51 changes: 51 additions & 0 deletions setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

# Minimum required versions
NODE_MAJOR=16
YARN_VERSION="1.22.22"

log() { echo "[setup] $*"; }
err() { echo "[setup] ERROR: $*" >&2; exit 1; }

# Install nvm if not present
if ! command -v nvm &>/dev/null && [ ! -f "$HOME/.nvm/nvm.sh" ]; then
log "Installing nvm..."
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
fi

# Load nvm
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# shellcheck source=/dev/null
[ -s "$NVM_DIR/nvm.sh" ] && source "$NVM_DIR/nvm.sh"

if ! command -v nvm &>/dev/null; then
err "nvm not found after install — open a new shell and re-run this script"
fi

# Install and use the required Node version
log "Installing Node.js $NODE_MAJOR (LTS)..."
nvm install "$NODE_MAJOR"
nvm use "$NODE_MAJOR"
log "Node $(node --version)"

# Install Yarn classic (v1)
if ! command -v yarn &>/dev/null || [[ "$(yarn --version)" != 1.* ]]; then
log "Installing Yarn $YARN_VERSION..."
npm install -g "yarn@$YARN_VERSION"
fi
log "Yarn $(yarn --version)"

# Install project dependencies
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

log "Installing project dependencies..."
yarn install --frozen-lockfile

log ""
log "Setup complete. Available commands:"
log " yarn start — start the dev server"
log " yarn build — production build"
log " yarn test — run tests"
log " yarn lint — lint TypeScript/JavaScript"
2 changes: 2 additions & 0 deletions src/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import registerConfig from 'plugins/config';
import registerPendingList from 'plugins/lists/pending';
import registerMovieList from 'plugins/lists/movies';
import registerEntryList from 'plugins/lists/entry';
import registerBackfill from 'plugins/backfill';
import registerOperations from 'core/operations';
import { AuthContainer } from 'core/auth/hooks';
import { TaskContainer } from 'plugins/tasks/hooks';
Expand All @@ -33,6 +34,7 @@ registerSeries();
registerPendingList();
registerEntryList();
registerMovieList();
registerBackfill();

const globals = css`
html {
Expand Down
11 changes: 9 additions & 2 deletions src/common/inputs/formik/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ export type Props = TextFieldProps & {
name: string;
};

const TextField: FC<Props> = ({ name, ...props }) => {
const TextField: FC<Props> = ({ name, helperText, ...props }) => {
const [field, { touched, error }] = useField(name);

return <BaseTextField error={touched && !!error} helperText={error} {...field} {...props} />;
return (
<BaseTextField
error={touched && !!error}
helperText={touched && error ? error : helperText}
{...field}
{...props}
/>
);
};

export default TextField;
2 changes: 1 addition & 1 deletion src/core/layout/SideNav/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ const SideNav: FC<Props> = ({ sidebarOpen = false, onClose, className }) => {
width: inherit;
`}
>
{routes.map(route => (
{routes.filter(route => !route.hidden).map(route => (
<Entry key={route.path} onClick={handleClick(route)} {...route} />
))}
</List>
Expand Down
4 changes: 2 additions & 2 deletions src/core/layout/__snapshots__/Layout.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -395,10 +395,10 @@ exports[`common/layout renders correctly 1`] = `
Array [
Object {
"map": undefined,
"name": "1a7v7et",
"name": "10zn699",
"next": undefined,
"styles": "
overflow-y: auto;
overflow-y: scroll;
padding: 1.6rem;
height: 100%;

Expand Down
2 changes: 1 addition & 1 deletion src/core/layout/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const leavingTransition = (theme: Theme) => css`
`;

export const content = (theme: Theme) => css`
overflow-y: auto;
overflow-y: scroll;
padding: ${theme.typography.pxToRem(theme.spacing(2))};
height: 100%;

Expand Down
1 change: 1 addition & 0 deletions src/core/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Plugin {
displayName: string;
icon: ComponentType;
cardComponent?: ComponentType;
hidden?: boolean;
}
export type PluginMap = Record<string, Plugin>;
export type PluginUpdateHandler = (e: CustomEvent<PluginMap>) => void;
Expand Down
3 changes: 2 additions & 1 deletion src/core/routes/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ export const useGetRoutes = () => {
const { pluginMap } = useContainer(PluginContainer);
const routes: Route[] = useMemo(
() =>
Object.entries(pluginMap).flatMap(([path, { component, displayName, icon }]) =>
Object.entries(pluginMap).flatMap(([path, { component, displayName, icon, hidden }]) =>
component
? [
{
path,
component,
Icon: icon,
name: displayName,
hidden,
},
]
: [],
Expand Down
1 change: 1 addition & 0 deletions src/core/routes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export interface Route {
name: string;
Icon: ComponentType;
path: string;
hidden?: boolean;
}
Loading