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

feat: show artifact icon from metadata #7

Merged
merged 1 commit into from
Dec 4, 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: 5 additions & 1 deletion src/app/api/artifacts/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { client } from '../client';
import { assertSuccessResponse } from '../utils/assertSuccessResponse';
import { fetchEntity } from '../utils/fetchEntity';
import { decodeEntityWithMetadata } from '../utils/metadata';
import { ArtifactShared } from './types';

type Params = {
id: string;
Expand All @@ -23,5 +25,7 @@ async function readArtifactShared({ id, secret }: Params) {
export async function fetchArtifactShared(params: Params) {
const artifact = await fetchEntity(() => readArtifactShared(params));

return artifact;
return artifact
? decodeEntityWithMetadata<ArtifactShared>(artifact)
: artifact;
}
15 changes: 14 additions & 1 deletion src/app/api/artifacts/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
import { ARTIFACT_ICONS } from '@/modules/artifacts/ArtifactIcon';
import { paths } from '../schema';
import { EntityWithDecodedMetadata } from '../types';

export type ArtifactShared =
export type ArtifactSharedResult =
paths['/v1/artifacts/{artifact_id}/shared']['get']['responses']['200']['content']['application/json'];

export interface ArtifactSharedMetadata {
icon?: ArtifactSharedIcon;
}

export type ArtifactSharedIcon = keyof typeof ARTIFACT_ICONS;

export type ArtifactShared = EntityWithDecodedMetadata<
ArtifactSharedResult,
ArtifactSharedMetadata
>;
20 changes: 20 additions & 0 deletions src/app/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,23 @@ import { components } from './schema';
export type ApiErrorResponse = components['schemas']['ErrorResponse'];

export type ApiErrorCode = ApiErrorResponse['error']['code'];

export type EntityMetadata = {
[key: string]: any;
};

export type ApiMetadata = {
[key: string]: string;
};

export type EntityResultWithMetadata<T> = Omit<T, 'uiMetadata'> & {
metadata?: ApiMetadata;
};

export type EntityWithEncodedMetadata<T> = Omit<T, 'uiMetadata'> & {
metadata: ApiMetadata;
};

export type EntityWithDecodedMetadata<T, M> = Omit<T, 'metadata'> & {
uiMetadata: M;
};
78 changes: 78 additions & 0 deletions src/app/api/utils/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { isNotNull } from '@/utils/helpers';
import {
EntityMetadata,
ApiMetadata,
EntityResultWithMetadata,
EntityWithEncodedMetadata,
} from '../types';

const API_KEY_PREFIX = '$ui_';

export function encodeMetadata<T extends EntityMetadata>(
metadata?: T
): ApiMetadata {
const encoded: ApiMetadata = {};

for (const clientKey in metadata) {
const value = metadata[clientKey];
const apiKey = `${API_KEY_PREFIX}${clientKey}`;

if (isNotNull(value)) {
if (typeof value === 'boolean' || typeof value === 'object') {
try {
encoded[apiKey] = JSON.stringify(value);
} catch {
encoded[apiKey] = String(value);
}
} else {
encoded[apiKey] = String(value);
}
}
}

return encoded;
}

export function decodeMetadata<T extends EntityMetadata>(
metadata?: ApiMetadata
): T {
const decoded: EntityMetadata = {};

for (const apiKey in metadata) {
const value = metadata[apiKey];
const clientKey = apiKey.startsWith(API_KEY_PREFIX)
? apiKey.slice(API_KEY_PREFIX.length)
: apiKey;

if (isNotNull(value)) {
try {
const parsedValue = JSON.parse(value);

decoded[clientKey] = isNotNull(parsedValue) ? parsedValue : value;
} catch {
decoded[clientKey] = value;
}
}
}

return decoded as T;
}

export function decodeEntityWithMetadata<
T extends { uiMetadata: EntityMetadata },
>(apiEntity: EntityResultWithMetadata<T>): T {
const { metadata, ...rest } = apiEntity;

return {
...rest,
uiMetadata: decodeMetadata<T['uiMetadata']>(metadata),
} as T;
}

export function encodeEntityWithMetadata<
T extends { uiMetadata: EntityMetadata },
>(entity: T): EntityWithEncodedMetadata<T> {
const { uiMetadata, ...rest } = entity;

return { ...rest, metadata: encodeMetadata<T['uiMetadata']>(uiMetadata) };
}
6 changes: 5 additions & 1 deletion src/app/artifacts/[artifactId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export default async function ArtifactPage({ params, searchParams }: Props) {

return (
<>
<Header heading={artifact.name} tooltip={artifact.description} />
<Header
heading={artifact.name}
tooltip={artifact.description}
icon={artifact.uiMetadata.icon}
/>

<Main className="flex py-2 md:py-6">
<Container className="px-2 md:px-6">
Expand Down
60 changes: 60 additions & 0 deletions src/modules/artifacts/ArtifactIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { ArtifactSharedIcon } from '@/app/api/artifacts/types';
import {
Book,
Bot,
ChartLineSmooth,
Chat,
Chemistry,
Code,
ColorPalette,
CurrencyDollar,
EarthFilled,
FaceWink,
Favorite,
Gamification,
GroupPresentation,
Idea,
Lightning,
Pen,
Rocket,
TextShortParagraph,
ToolKit,
UserMultiple,
} from '@carbon/icons-react';

interface Props {
name: ArtifactSharedIcon;
}

export function ArtifactIcon({ name }: Props) {
const Icon = ARTIFACT_ICONS[name];

return (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-2 bg-coolGray-10 dark:bg-coolGray-80">
<Icon size={16} />
</div>
);
}

export const ARTIFACT_ICONS = {
ColorPalette,
Pen,
FaceWink,
Idea,
Book,
Chat,
Chemistry,
ToolKit,
UserMultiple,
CurrencyDollar,
Rocket,
TextShortParagraph,
Code,
Bot,
Favorite,
EarthFilled,
GroupPresentation,
Lightning,
Gamification,
ChartLineSmooth,
};
18 changes: 6 additions & 12 deletions src/modules/artifacts/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
import { ArtifactSharedIcon } from '@/app/api/artifacts/types';
import { Button } from '@/components/ui/Button';
import { Container } from '@/components/ui/Container';
import { Tooltip } from '@/components/ui/Tooltip';
import {
CarbonIconType,
DirectionFork,
Information,
} from '@carbon/icons-react';
import { DirectionFork, Information } from '@carbon/icons-react';
import { ArtifactIcon } from './ArtifactIcon';

interface Props {
heading: string;
Icon?: CarbonIconType;
icon?: ArtifactSharedIcon;
tooltip?: string | null;
}

export function Header({ heading, Icon, tooltip }: Props) {
export function Header({ heading, icon, tooltip }: Props) {
return (
<header className="sticky left-0 top-0 border-b border-b-subtle bg-background py-3 md:py-5">
<Container>
<div className="grid items-center justify-center gap-x-4 gap-y-2 md:grid-cols-[1fr,auto,1fr]">
<div className="flex items-center gap-x-1 md:col-start-2">
{Icon && (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-2 bg-coolGray-10 dark:bg-coolGray-80">
<Icon size={16} />
</div>
)}
{icon && <ArtifactIcon name={icon} />}

<h1 className="text-base font-semibold text-coolGray-100 dark:text-white">
{heading}
Expand Down
Loading