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

Improve static rendering #346

Merged
merged 4 commits into from
Sep 23, 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
6 changes: 6 additions & 0 deletions .changeset/perfect-pets-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@watching/design': patch
'@watching/clips': patch
---

Add basic support for `action` attribute on button
6 changes: 6 additions & 0 deletions .changeset/pretty-games-grab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@watching/tools': patch
'@watching/cli': patch
---

Update clip static content uploading
112 changes: 71 additions & 41 deletions app/server/graphql/resolvers/apps/ClipsExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export const ClipsExtensionVersion = createResolverWithGid(
assets: ({scriptUrl}) => (scriptUrl ? [{source: scriptUrl}] : []),
translations: ({translations}) =>
translations ? JSON.stringify(translations) : null,
// Database needs to store the exact right shapes in its JSON fields
extends: ({extends: supports}) => (supports as any) ?? [],
settings: ({settings}) => (settings as any) ?? {fields: []},
},
Expand Down Expand Up @@ -435,13 +436,10 @@ export const ClipsExtensionInstallation = createResolverWithGid(

if (loading == null) return null;

const {parseLoadingHtml} = await import('@watching/tools/loading');

return {
ui: loading?.ui
? {
html: loading.ui,
tree: JSON.stringify(parseLoadingHtml(loading.ui)),
}
: null,
};
Expand Down Expand Up @@ -528,6 +526,30 @@ export const WatchThrough = createResolver('WatchThrough', {
},
});

async function parseExtensionPointTarget(extensionPoint: string) {
const {validateExtensionPoint} = await import('@watching/tools/extension');

if (!validateExtensionPoint(extensionPoint)) {
throw new Error(`Unsupported extension point: ${extensionPoint}`);
}

return extensionPoint;
}

async function parseLiveQuery(liveQuery: string) {
const {validateAndNormalizeLiveQuery} = await import(
'@watching/tools/extension'
);
return validateAndNormalizeLiveQuery(liveQuery);
}

async function parseLoadingUI(loadingUI: string) {
const {validateAndNormalizeLoadingUI} = await import(
'@watching/tools/extension'
);
return validateAndNormalizeLoadingUI(loadingUI);
}

async function createStagedClipsVersion({
code,
appId,
Expand Down Expand Up @@ -580,45 +602,53 @@ async function createStagedClipsVersion({
translations: translations && JSON.parse(translations),
extends: supports
? await Promise.all(
supports.map(async ({target, liveQuery, loading, conditions}) => {
return {
target,
liveQuery: liveQuery
? await (async () => {
const [
{parse},
{toGraphQLOperation, cleanGraphQLDocument},
] = await Promise.all([
import('graphql'),
import('@quilted/graphql-tools'),
]);

return toGraphQLOperation(
cleanGraphQLDocument(parse(liveQuery)),
).source;
})()
: undefined,
loading: loading?.ui
? {
ui: await (async () => {
const {parseLoadingHtml, serializeLoadingHtml} =
await import('@watching/tools/loading');

return serializeLoadingHtml(
parseLoadingHtml(loading.ui!),
);
})(),
supports.map(
async ({target, modules, liveQuery, loading, conditions}) => {
const [
parsedTarget,
parsedModules,
parsedLiveQuery,
parsedLoadingUI,
] = await Promise.all([
parseExtensionPointTarget(target),
Promise.all(
(modules ?? []).map(
async ({content, contentType = 'HTML'}) => {
if (content.length > 10_000) {
throw new Error(
`Clip module content for ${target} is too long`,
);
}

// TODO: validate HTML content

return {content, contentType};
},
),
),
liveQuery ? parseLiveQuery(liveQuery) : undefined,
loading?.ui ? parseLoadingUI(loading.ui) : undefined,
]);

return {
target: parsedTarget,
modules: parsedModules,
liveQuery: parsedLiveQuery,
loading: parsedLoadingUI
? {
ui: parsedLoadingUI,
}
: undefined,
conditions: conditions?.map((condition) => {
if (condition?.series?.handle == null) {
throw new Error(`Unknown condition: ${condition}`);
}
: undefined,
conditions: conditions?.map((condition) => {
if (condition?.series?.handle == null) {
throw new Error(`Unknown condition: ${condition}`);
}

return condition;
}),
};
}) as any,

return condition;
}),
};
},
) as any,
)
: [],
settings: settings?.fields
Expand Down
1 change: 0 additions & 1 deletion app/server/graphql/schema.d.ts.map

This file was deleted.

145 changes: 54 additions & 91 deletions app/shared/clips/Clip/Clip.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type {ComponentType} from 'preact';
import {useEffect, useRef} from 'preact/hooks';

import {type ExtensionPoint} from '@watching/clips';
import {RemoteRootRenderer} from '@remote-dom/preact/host';
import {
Style,
Popover,
Stack,
InlineStack,
BlockStack,
InlineGrid,
View,
Expand All @@ -17,25 +14,22 @@ import {
Icon,
Button,
Section,
SkeletonButton,
SkeletonText,
SkeletonTextBlock,
SkeletonView,
} from '@lemon/zest';
import {classes} from '@lemon/css';
import type {ThreadRendererInstance} from '@watching/thread-render';

import {useGraphQLMutation} from '~/shared/graphql.ts';

import {ClipsExtensionPointBeingRenderedContext} from '../context.ts';
import {useClipsManager} from '../react.tsx';
import {
type ClipsExtensionPoint,
type ClipsExtensionPointInstance,
type ClipsExtensionPointInstanceContext,
type ClipsExtensionPointInstanceLoadingElement,
} from '../extension.ts';

import {ClipSettings} from './ClipSettings.tsx';
import {ClipStaticRenderer} from './ClipStaticRenderer.tsx';

import styles from './Clip.module.css';
import uninstallClipsExtensionFromClipMutation from './graphql/UninstallClipsExtensionFromClipMutation.graphql';
Expand All @@ -57,56 +51,58 @@ export function Clip<Point extends ExtensionPoint>({
const renderer = local ?? installed;

return (
<Section>
<BlockStack spacing>
<ContentAction
overlay={
<Popover inlineAttachment="start">
{installed?.instance.value && (
<Section padding>
<ClipSettings
id={extension.id}
instance={installed.instance.value}
/>
</Section>
)}
<Menu>
<ViewAppAction />
{renderer && <RestartClipButton instance={renderer} />}
{extension.installed && (
<UninstallClipButton extension={extension} />
<ClipsExtensionPointBeingRenderedContext.Provider value={extension}>
<Section>
<BlockStack spacing>
<ContentAction
overlay={
<Popover inlineAttachment="start">
{installed?.instance.value && (
<Section padding>
<ClipSettings
id={extension.id}
instance={installed.instance.value}
/>
</Section>
)}
{extension.installed && <ReportIssueButton />}
</Menu>
</Popover>
}
>
<InlineGrid sizes={['auto', 'fill']} spacing="small">
<View
display="inlineFlex"
background="emphasized"
border="subdued"
cornerRadius
alignment="center"
blockSize={Style.css`2.5rem`}
inlineSize={Style.css`2.5rem`}
>
<Icon source="app" />
</View>
<BlockStack>
<Text emphasis accessibilityRole="heading">
{name}
</Text>
<Text emphasis="subdued" size="small">
from app <Text emphasis>{app.name}</Text>
</Text>
</BlockStack>
</InlineGrid>
</ContentAction>

{renderer && <ClipInstanceRenderer renderer={renderer} />}
</BlockStack>
</Section>
<Menu>
<ViewAppAction />
{renderer && <RestartClipButton instance={renderer} />}
{extension.installed && (
<UninstallClipButton extension={extension} />
)}
{extension.installed && <ReportIssueButton />}
</Menu>
</Popover>
}
>
<InlineGrid sizes={['auto', 'fill']} spacing="small">
<View
display="inlineFlex"
background="emphasized"
border="subdued"
cornerRadius
alignment="center"
blockSize={Style.css`2.5rem`}
inlineSize={Style.css`2.5rem`}
>
<Icon source="app" />
</View>
<BlockStack>
<Text emphasis accessibilityRole="heading">
{name}
</Text>
<Text emphasis="subdued" size="small">
from app <Text emphasis>{app.name}</Text>
</Text>
</BlockStack>
</InlineGrid>
</ContentAction>

{renderer && <ClipInstanceRenderer renderer={renderer} />}
</BlockStack>
</Section>
</ClipsExtensionPointBeingRenderedContext.Provider>
);
}

Expand Down Expand Up @@ -215,16 +211,6 @@ function ClipInstanceRenderer<Point extends ExtensionPoint>({
);
}

const LOADING_COMPONENT_MAP = new Map<string, ComponentType<any>>([
['ui-stack', Stack],
['ui-block-stack', BlockStack],
['ui-inline-stack', InlineStack],
['ui-skeleton-button', SkeletonButton],
['ui-skeleton-text', SkeletonText],
['ui-skeleton-text-block', SkeletonTextBlock],
['ui-skeleton-view', SkeletonView],
]);

function ClipsInstanceRendererLoading<Point extends ExtensionPoint>({
instance,
}: {
Expand All @@ -234,28 +220,5 @@ function ClipsInstanceRendererLoading<Point extends ExtensionPoint>({

if (loadingUi == null) return null;

const renderNode = (
child: ClipsExtensionPointInstanceLoadingElement['children'][number],
index: number,
) => {
if (typeof child === 'string') {
return <>{child}</>;
}

const {type, properties, children} = child;

const Component = LOADING_COMPONENT_MAP.get(type);

if (Component == null) {
throw new Error(`Unknown loading component: ${type}`);
}

return (
<Component key={index} {...properties}>
{children.map(renderNode)}
</Component>
);
};

return <>{loadingUi.map(renderNode)}</>;
return <ClipStaticRenderer content={loadingUi} />;
}
Loading