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
5 changes: 5 additions & 0 deletions .changeset/good-pants-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"strapi-plugin-webtools": patch
---

Feature/try webtools inscentives
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ Builds all packages using Turborepo's caching and dependency graph.

## Testing

### Node version

Always activate Node 20 before running yarn commands:

```bash
nvm use # reads .nvmrc (Node 20)
```

The repo uses **Yarn 4.11.0** via `yarnPath` in `.yarnrc.yml`. Running `yarn` anywhere inside the repo automatically uses this version — even in subdirectories like `playground/`.

### Unit Tests

```bash
Expand All @@ -69,6 +79,8 @@ Unit tests are located in `__tests__` directories:

Uses Jest with ts-jest preset. Test files: `*.test.ts` or `*.test.js`

`playground/.env` is **force-committed** to git with placeholder values (`tobemodified`) so CI can boot Strapi. Do not add real secrets to it — use your local `.env` override for `WEBTOOLS_LICENSE_KEY` and similar.

### Integration Tests

```bash
Expand Down Expand Up @@ -184,6 +196,10 @@ Addons are Strapi plugins with a special flag in their `package.json`:

**Discovery**: Core scans `enabledPlugins` at runtime for this flag

**Addon detection in admin UI**: Always match on `addon.info.name` (the Strapi plugin name, e.g. `webtools-addon-redirects`), **not** on UI labels which are translatable. Strip the npm scope before comparing: `@pluginpal/webtools-addon-redirects` → `webtools-addon-redirects`.

**Pro addons** (not open-source) are defined in `admin/constants/pro-addons.ts`. When not installed, they appear in the sidebar as locked items (`LockedAddonMenuItem`) that open a `TrialModal`. The `packageName` field in `ProAddon` is the full scoped npm name.

**Integration**: Addons inject components via named zones:
- `webtoolsRouter`: Adds routes to main navigation
- `webtoolsSidePanel`: Adds components to content editor sidebar
Expand Down Expand Up @@ -296,6 +312,23 @@ See `packages/addons/sitemap` as reference:
```
4. Optionally extend content types in server `bootstrap()`

## ESLint rules to watch

- **`max-len`**: 100 character limit. Long inline comments will fail.
- **`no-confusing-arrow` + `implicit-arrow-linebreak`**: Arrow functions with ternaries must use a block body:
```typescript
// ✗ fails linting
const fn = (x: string) =>
x.includes('/') ? x.split('/')[1] : x;

// ✓ correct
const fn = (x: string) => {
if (x.includes('/')) return x.split('/')[1];
return x;
};
```
- **`@typescript-eslint/no-unnecessary-type-assertion`**: Remove casts that TypeScript already infers.

## Release Process

This project uses Changesets for version management:
Expand Down
52 changes: 52 additions & 0 deletions packages/core/admin/components/LockedAddonMenuItem/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import React, { useState } from 'react';
import { useIntl } from 'react-intl';
import {
SubNavLink,
Tooltip,
} from '@strapi/design-system';
import { Lock } from '@strapi/icons';
import { LockedAddonMenuItemProps } from '../../types/pro-addons';
import TrialModal from '../TrialModal';

const LockedAddonMenuItem: React.FC<LockedAddonMenuItemProps> = ({ addon }) => {
const { formatMessage } = useIntl();
const [isModalOpen, setIsModalOpen] = useState(false);

const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
setIsModalOpen(true);
};

return (
<>
<Tooltip
description={formatMessage({
id: 'webtools.sidebar.locked_addon.tooltip',
defaultMessage: 'Start free trial to unlock',
})}
>
<SubNavLink
onClick={handleClick}
style={{
cursor: 'pointer',
opacity: 0.5,
pointerEvents: 'auto',
}}
>
<span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<Lock width="12px" height="12px" />
{addon.name}
</span>
</SubNavLink>
</Tooltip>

<TrialModal
addon={addon}
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
/>
</>
);
};

export default LockedAddonMenuItem;
71 changes: 71 additions & 0 deletions packages/core/admin/components/TrialCallToAction/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { useIntl } from 'react-intl';
import {
Box,
Flex,
Typography,
Button,
} from '@strapi/design-system';
import { TRIAL_URL } from '../../constants/pro-addons';

interface TrialCallToActionProps {
variant?: 'banner' | 'card' | 'inline';
}

const TrialCallToAction: React.FC<TrialCallToActionProps> = ({ variant = 'card' }) => {
const { formatMessage } = useIntl();

const content = (
<Flex direction="column" alignItems="center" gap={2}>
<Typography variant="delta" textAlign="center">
{formatMessage({
id: 'webtools.overview.trial_cta.title_short',
defaultMessage: 'Ready to unlock Pro features?',
})}
</Typography>

<Typography variant="omega" textColor="neutral600" textAlign="center">
{formatMessage({
id: 'webtools.overview.trial_cta.subtitle',
defaultMessage: 'Start your free 7-day trial - includes Redirects & Links addons',
})}
</Typography>

<Box marginTop={3}>
<Button
variant="default"
tag="a"
href={TRIAL_URL}
target="_blank"
rel="noopener noreferrer"
>
{formatMessage({
id: 'webtools.overview.trial_cta.button_short',
defaultMessage: 'Start Free Trial',
})}
</Button>
</Box>
</Flex>
);

if (variant === 'inline') {
return content;
}

return (
<Box
hasRadius
background="neutral0"
shadow="tableShadow"
paddingTop={8}
paddingBottom={8}
paddingRight={8}
paddingLeft={8}
style={variant === 'banner' ? { width: '100%' } : undefined}
>
{content}
</Box>
);
};

export default TrialCallToAction;
Loading
Loading