Skip to content

Commit

Permalink
Improve static rendering (#346)
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed Sep 23, 2024
1 parent a764f6d commit 65666e4
Show file tree
Hide file tree
Showing 24 changed files with 625 additions and 337 deletions.
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

0 comments on commit 65666e4

Please sign in to comment.