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

Block plugins and inline blocks #376

Merged
merged 22 commits into from
May 22, 2024
Merged
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
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ To run the project, open the command line in the project's root directory and en

The above `npm start` executes the `vite dev` command of `packages/editor` and watches for changes to this main package.

## Watch changes
<!-- ## Watch changes

npm run watch

You might also be making changes to other packages in the `packages` directory. To continuously watch and compile for changes, open a new terminal and run `npm run watch`.
You might also be making changes to other packages in the `packages` directory. To continuously watch and compile for changes, open a new terminal and run `npm run watch`. -->

## Testing

Expand Down
12,160 changes: 7,705 additions & 4,455 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@
"patch-package": "patch-package",
"postinstall": "patch-package",
"playwright:dev": "npm run playwright:dev --workspaces",
"playwright:preview": "npm run playwright:preview --workspaces",
"playwright:preview": "npm run playwright:preview --workspace=packages/editor",
"install-playwright": "npx playwright install --with-deps",
"test": "npm run test --workspaces",
"unittest:vitest": "npm run unittest:vitest --workspaces",
"wip:unittest:vitest:coverage": "vitest run --coverage -r packages/xxx",
"build": "npm run build --workspaces",
"build:react": "npm run build:react --workspace=packages/editor",
"lint": "npm run lint --workspaces",
"watch": "npm run build && npm run --parallel watch",
"start": "npm run start-react",
"start-react": "npm run start --workspace=packages/editor",
"start:preview": "npm run preview --workspace=packages/editor",
Expand Down
8 changes: 5 additions & 3 deletions packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
"private": true,
"dependencies": {
"react-confetti-explosion": "^2.1.2",
"@hocuspocus/provider": "^2.4.0",
"@hocuspocus/provider": "2.4.0",
"@atlaskit/atlassian-navigation": "^2.6.13",
"@atlaskit/avatar": "^21.3.7",
"@atlaskit/breadcrumbs": "^11.10.5",
"@atlaskit/button": "^16.8.2",
"@atlaskit/css-reset": "^6.5.3",
"@atlaskit/checkbox": "^13.3.0",
"@atlaskit/dropdown-menu": "^11.10.5",
"@atlaskit/empty-state": "^7.6.3",
"@atlaskit/flag": "^15.2.15",
Expand All @@ -26,7 +27,7 @@
"@atlaskit/textfield": "^5.6.3",
"@atlaskit/tree": "^8.8.5",
"@tiptap/core": "^2.0.4",
"@blocknote/core": "^0.9.3",
"@blocknote/core": "^0.13.2",
"@emotion/react": "^11.4.0",
"@supabase/auth-ui-react": "^0.4.5",
"@supabase/auth-ui-shared": "^0.1.7",
Expand Down Expand Up @@ -57,14 +58,15 @@
"vscode-lib": "^0.1.2",
"web-vitals": "^1.0.1",
"y-indexeddb": "9.0.6",
"y-websocket": "^2.0.3",
"y-protocols": "^1.0.5",
"yjs": "^13.6.4",
"react-inspector": "^6.0.1"
},
"scripts": {
"copytypes:self": "tsc --declaration --emitDeclarationOnly --noEmit false --composite false --declarationDir ./public/types/@typecell-org/editor",
"copytypes:externaldep": "mkdir -p public/types/$npm_config_pkgname && cp -rf ../../node_modules/$npm_config_pkgname/. public/types/$npm_config_pkgname",
"copytypes:allexternaldeps": "npm run copytypes:externaldep --pkgname=@types/react && npm run copytypes:externaldep --pkgname=@types/scheduler && npm run copytypes:externaldep --pkgname=@types/prop-types && npm run copytypes:externaldep --pkgname=csstype",
"copytypes:allexternaldeps": "npm run copytypes:externaldep --pkgname=@types/react && npm run copytypes:externaldep --pkgname=@types/prop-types && npm run copytypes:externaldep --pkgname=csstype",
"copytypes:dep": "mkdir -p public/types/@typecell-org/$npm_config_pkgname && cp -rf ../$npm_config_pkgname/types/. public/types/@typecell-org/$npm_config_pkgname",
"copytypes:alldeps": "npm run copytypes:dep --pkgname=util && npm run copytypes:dep --pkgname=engine && npm run copytypes:dep --pkgname=frame",
"copytypes": "npm run copytypes:self && npm run copytypes:alldeps && npm run copytypes:allexternaldeps",
Expand Down
1 change: 1 addition & 0 deletions packages/editor/public/_docs/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"manual/3. Reactive variables.md",
"manual/4. Inputs.md",
"manual/5. Imports and NPM.md",
"manual/6. Plugins.md",
"README.md"
]
}
85 changes: 85 additions & 0 deletions packages/editor/public/_docs/manual/6. Plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Plugins

A powerful concept that TypeCell explores is _End User Programming_. In TypeCell, it's possible to customize the program you're using (a Notion-style document editor) with new capabilities, _right from within the application itself_. This means you can modify the way TypeCell works, without changing the source code, but just by creating new code in TypeCell code blocks.

## Map block

When using software like Notion, Google Docs, or Word, you're limited to the blocks they provide (paragraphs, images, lists, tables, etc.). What if you want to add an interactive map, or chart to your document? Let's explore how this can be done in TypeCell.

Let's first set up the code to render a Map, based on _react-map-gl_.

### Map code

First, let's set up some reactive variables for our map component:

```typescript
export let zoom = 1;
export let latitude = 1;
export let longitude = 1;
export let markers: Array<{
latitude: number;
longitude: number;
color: string;
text: string;
}> = [];
```

And import the required CSS stylesheet:

```typescript
import css from "maplibre-gl/dist/maplibre-gl.css";
export { css };
```

Now, let's create the main code that renders our map component:

```typescript
import MapLibre, { Marker, Source, Layer, Popup } from "react-map-gl/maplibre";
import maplibregl from "maplibre-gl";

export const map = (
<div style={{ width: 700, height: 400 }}>
<MapLibre
onMove={(e) => {
$.zoom = e.viewState.zoom;
$.latitude = e.viewState.latitude;
$.longitude = e.viewState.longitude;
}}
longitude={$.longitude}
latitude={$.latitude}
zoom={$.zoom}
mapStyle="https://demotiles.maplibre.org/style.json">
{$.markers.map((m, i) => (
<Marker
key={i}
latitude={m.latitude}
longitude={m.longitude}
color={m.color}
popup={m.text ? new maplibregl.Popup().setText(m.text) : undefined}
/>
))}
</MapLibre>
</div>
);
```

### Register a plugin

Now, we can register the _Map_ variable as a Block that can be added to any document. Try it out by typing "/" in this document, or clicking the + icon next to a block. You'll see that you can now add Map blocks to the document.

```typescript
// Plugin registration
typecell.editor.registerBlock({
name: "Map",
blockVariable: "map",
// Variables for properties screen that's auto-generated
// and shows when clicking the settings-gear icon
settings: {
latitude: true,
longitude: true,
zoom: true,
},
});
```

Your local environment will keep track of registered plugins. Now that you've visited this page, you can reuse the Plugin registered here in any other document you create. Try this out by signing in, and going to a document in your own workspace. Access the document / plugin menu via the top-right dots.
37 changes: 24 additions & 13 deletions packages/editor/src/app/documentRenderers/richtext/FrameHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,43 +65,54 @@ export function FrameHost(props: {
>();

const methods = {
markPlugins: async (identifierStr: string, value: boolean) => {
const identifier = parseIdentifier(identifierStr);
props.sessionStore.documentCoordinator?.markPlugins(identifier, value);
},
processYjsMessage: async (message: ArrayBuffer) => {
provider.onMessage(message, "penpal");
},
registerTypeCellModuleCompiler: async (moduleName: string) => {
if (moduleManagers.has(moduleName)) {
console.warn("already has moduleManager for", moduleName);
return;
}
resolveModuleName: async (moduleName: string) => {
if (!moduleName.startsWith("!")) {
throw new Error("invalid module name");
}
const identifierStr = moduleName.substring(1);

const identifier = parseIdentifier(moduleName.substring(1));
const identifierStr = identifier.toString();
return identifierStr;
},
registerTypeCellModuleCompiler: async (identifierStr: string) => {
const identifier = parseIdentifier(identifierStr);
if (moduleManagers.has(identifierStr)) {
console.warn("already has moduleManager for", identifierStr);
return identifierStr;
}

const provider = new DocumentResourceModelProvider(
identifier,
props.sessionStore,
);

const forwarder = new ModelForwarder(
"modules/" + moduleName,
"modules/" + identifierStr,
provider,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
connectionMethods.current!,
);
moduleManagers.set(moduleName, { provider, forwarder });

moduleManagers.set(identifierStr, { provider, forwarder });
await forwarder.initialize();
return identifier.toString();
return identifierStr;
},
unregisterTypeCellModuleCompiler: async (moduleName: string) => {
const moduleManager = moduleManagers.get(moduleName);
unregisterTypeCellModuleCompiler: async (identifierStr: string) => {
const moduleManager = moduleManagers.get(identifierStr);
if (!moduleManager) {
console.warn("no moduleManager for", moduleName);
console.warn("no moduleManager for", identifierStr);
return;
}
moduleManager.provider.dispose();
moduleManager.forwarder.dispose();
moduleManagers.delete(moduleName);
moduleManagers.delete(identifierStr);
},
};

Expand Down
6 changes: 3 additions & 3 deletions packages/editor/src/app/main/components/ProfilePopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { toProfilePage } from "../../routes/routes";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Trigger = observer((props: any) => {
const { triggerRef, isSelected, testId, ...passProps } = props;
const { triggerRef, isSelected, testId, sessionStore, ...passProps } = props;
return (
<Profile
testId="profile-button"
icon={
<Avatar
name={props.sessionStore.loggedInUserId}
src={props.sessionStore.profile?.avatar_url || undefined}
name={sessionStore.loggedInUserId}
src={sessionStore.profile?.avatar_url || undefined}
size="32"
round={true}
textSizeRatio={2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ import { DocumentResource } from "../../../../store/DocumentResource";
import { SessionStore } from "../../../../store/local/SessionStore";
import {
ClosePermissionsDialog,
ClosePluginDialog,
IsPermissionsDialogOpen,
IsPluginDialogOpen,
OpenPermissionsDialog,
OpenPluginDialog,
} from "../../../routes/routes";
import { MenuBar } from "../menuBar/MenuBar";

Expand All @@ -26,6 +29,7 @@ import SupabasePermissionsDialog from "../../../supabase-auth/routes/permissions
import { Breadcrumb } from "./Breadcrumb";
import styles from "./DocumentMenu.module.css";
import { ForkAlert } from "./ForkAlert";
import PluginDialog from "./PluginDialog";
import { ShareButton } from "./ShareButton";

type Props = {
Expand All @@ -36,7 +40,7 @@ type Props = {
// TODO: move?
function userCanEditPermissions(
sessionStore: SessionStore,
identifier: Identifier
identifier: Identifier,
) {
if (identifier instanceof HttpsIdentifier) {
return false;
Expand All @@ -56,7 +60,7 @@ export const DocumentMenu: React.FC<Props> = observer((props) => {
const { sessionStore } = props;
const canEditPermissions = userCanEditPermissions(
sessionStore,
props.document.identifier
props.document.identifier,
);
const location = useLocation();
const navigate = useNavigate();
Expand Down Expand Up @@ -127,13 +131,27 @@ export const DocumentMenu: React.FC<Props> = observer((props) => {
Permissions
</DropdownItem>
)}
{props.document instanceof DocumentResource && (
<DropdownItem onClick={() => OpenPluginDialog(navigate)}>
Plugins
</DropdownItem>
)}
</DropdownMenu>
</li>
</>
)}
</ul>
</aside>
{canEditPermissions && permissionsArea}
{props.document instanceof DocumentResource && (
<PluginDialog
close={() => ClosePluginDialog(navigate)}
isOpen={IsPluginDialogOpen(location)}
identifier={props.document.identifier}
sessionStore={sessionStore}
document={props.document}
/>
)}
</MenuBar>
);
});
Loading
Loading