Skip to content

Commit

Permalink
ISOM-1292: Add publish endpoint (#381)
Browse files Browse the repository at this point in the history
### TL;DR

This pull request introduces a publish endpoint and `PublishButton` component to the `AppNavbar` to enable page publishing.

### What changed?

- Relocated `AppNavbar` to a new folder structure: `components/AppNavbar/AppNavbar.tsx`.
- Added `PublishButton` component to `AppNavbar`.
- Implemented publish functionality in `PublishButton.tsx` with page and site parameters from the navigation path.
- Added a new API endpoint `publishPage` to handle the publishing logic and update the database accordingly.
- Updated `page.router.ts` and `page.service.ts` with publish logic.

Brief description of logic:
1. Check if there is a draft to be published, if yes, proceed
2. Fetch the page and the create a new version by incrementing the current version number
3. Use this newly created versionId to update the existing resource and set the draftBlobId to null

Pending Todos:
1. Implement trigger of CodeBuild

### How to test?

1. Navigate to the admin dashboard.
2. Click the new `Publish` button.
3. Verify page publication success or failure messages.

### Why make this change?

This feature allows users to publish pages directly from the navigation bar, improving the publishing workflow and user experience.
  • Loading branch information
harishv7 authored Aug 12, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 0414c28 commit f06c139
Showing 9 changed files with 374 additions and 130 deletions.
32 changes: 16 additions & 16 deletions apps/studio/prisma/generated/generatedEnums.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
export const ResourceState = {
Draft: "Draft",
Published: "Published",
} as const
export type ResourceState = (typeof ResourceState)[keyof typeof ResourceState]
Draft: "Draft",
Published: "Published"
} as const;
export type ResourceState = (typeof ResourceState)[keyof typeof ResourceState];
export const ResourceType = {
RootPage: "RootPage",
Page: "Page",
Folder: "Folder",
Collection: "Collection",
CollectionPage: "CollectionPage",
} as const
export type ResourceType = (typeof ResourceType)[keyof typeof ResourceType]
RootPage: "RootPage",
Page: "Page",
Folder: "Folder",
Collection: "Collection",
CollectionPage: "CollectionPage"
} as const;
export type ResourceType = (typeof ResourceType)[keyof typeof ResourceType];
export const RoleType = {
Admin: "Admin",
Editor: "Editor",
Publisher: "Publisher",
} as const
export type RoleType = (typeof RoleType)[keyof typeof RoleType]
Admin: "Admin",
Editor: "Editor",
Publisher: "Publisher"
} as const;
export type RoleType = (typeof RoleType)[keyof typeof RoleType];
194 changes: 94 additions & 100 deletions apps/studio/prisma/generated/generatedTypes.ts
Original file line number Diff line number Diff line change
@@ -2,103 +2,97 @@ import type { ColumnType, GeneratedAlways } from "kysely"

import type { ResourceState, ResourceType, RoleType } from "./generatedEnums"

export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>
export type Timestamp = ColumnType<Date, Date | string, Date | string>

export interface Blob {
id: GeneratedAlways<string>
/**
* @kyselyType(PrismaJson.BlobJsonContent)
* [BlobJsonContent]
*/
content: PrismaJson.BlobJsonContent
}
export interface Footer {
id: GeneratedAlways<number>
siteId: number
/**
* @kyselyType(PrismaJson.FooterJsonContent)
* [FooterJsonContent]
*/
content: PrismaJson.FooterJsonContent
}
export interface Navbar {
id: GeneratedAlways<number>
siteId: number
/**
* @kyselyType(PrismaJson.NavbarJsonContent)
* [NavbarJsonContent]
*/
content: PrismaJson.NavbarJsonContent
}
export interface Permission {
id: GeneratedAlways<number>
resourceId: string
userId: string
role: RoleType
}
export interface Resource {
id: GeneratedAlways<string>
title: string
permalink: string
siteId: number
parentId: string | null
publishedVersionId: string | null
draftBlobId: string | null
state: Generated<ResourceState | null>
type: ResourceType
}
export interface Site {
id: GeneratedAlways<number>
name: string
/**
* @kyselyType(PrismaJson.SiteJsonConfig)
* [SiteJsonConfig]
*/
config: PrismaJson.SiteJsonConfig
/**
* @kyselyType(PrismaJson.SiteThemeJson)
* [SiteThemeJson]
*/
theme: PrismaJson.SiteThemeJson | null
}
export interface SiteMember {
userId: string
siteId: number
}
export interface User {
id: string
name: string
email: string
phone: string
preferredName: string | null
}
export interface VerificationToken {
identifier: string
token: string
attempts: Generated<number>
expires: Timestamp
}
export interface Version {
id: GeneratedAlways<string>
versionNum: number
resourceId: string
blobId: string
publishedAt: Generated<Timestamp>
publishedBy: string
}
export interface DB {
Blob: Blob
Footer: Footer
Navbar: Navbar
Permission: Permission
Resource: Resource
Site: Site
SiteMember: SiteMember
User: User
VerificationToken: VerificationToken
Version: Version
}
export type Blob = {
id: GeneratedAlways<string>;
/**
* @kyselyType(PrismaJson.BlobJsonContent)
* [BlobJsonContent]
*/
content: PrismaJson.BlobJsonContent;
};
export type Footer = {
id: GeneratedAlways<number>;
siteId: number;
/**
* @kyselyType(PrismaJson.FooterJsonContent)
* [FooterJsonContent]
*/
content: PrismaJson.FooterJsonContent;
};
export type Navbar = {
id: GeneratedAlways<number>;
siteId: number;
/**
* @kyselyType(PrismaJson.NavbarJsonContent)
* [NavbarJsonContent]
*/
content: PrismaJson.NavbarJsonContent;
};
export type Permission = {
id: GeneratedAlways<number>;
resourceId: string;
userId: string;
role: RoleType;
};
export type Resource = {
id: GeneratedAlways<string>;
title: string;
permalink: string;
siteId: number;
parentId: string | null;
publishedVersionId: string | null;
draftBlobId: string | null;
state: Generated<ResourceState | null>;
type: ResourceType;
};
export type Site = {
id: GeneratedAlways<number>;
name: string;
/**
* @kyselyType(PrismaJson.SiteJsonConfig)
* [SiteJsonConfig]
*/
config: PrismaJson.SiteJsonConfig;
/**
* @kyselyType(PrismaJson.SiteThemeJson)
* [SiteThemeJson]
*/
theme: PrismaJson.SiteThemeJson | null;
};
export type SiteMember = {
userId: string;
siteId: number;
};
export type User = {
id: string;
name: string;
email: string;
phone: string;
preferredName: string | null;
};
export type VerificationToken = {
identifier: string;
token: string;
attempts: Generated<number>;
expires: Timestamp;
};
export type Version = {
id: GeneratedAlways<string>;
versionNum: number;
resourceId: string;
blobId: string;
publishedAt: Generated<Timestamp>;
publishedBy: string;
};
export type DB = {
Blob: Blob;
Footer: Footer;
Navbar: Navbar;
Permission: Permission;
Resource: Resource;
Site: Site;
SiteMember: SiteMember;
User: User;
VerificationToken: VerificationToken;
Version: Version;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Skeleton } from "@chakra-ui/react"
import { Button, useToast } from "@opengovsg/design-system-react"

import { withSuspense } from "~/hocs/withSuspense"
import { trpc } from "~/utils/trpc"

interface PublishButtonProps {
pageId: number
siteId: number
}

const SuspendablePublishButton = ({
pageId,
siteId,
}: PublishButtonProps): JSX.Element => {
const toast = useToast()
const utils = trpc.useUtils()

const [currPage] = trpc.page.readPage.useSuspenseQuery({ pageId, siteId })

const publishFailureMsg =
"Failed to publish page. Please contact Isomer support."
const publishSuccessMsg = "Page published successfully"

const { mutate, isLoading } = trpc.page.publishPage.useMutation({
onSuccess: async () => {
toast({
status: "success",
title: publishSuccessMsg,
})
await utils.page.readPage.invalidate({ pageId, siteId })
},
onError: async (error) => {
console.error(`Error occurred when publishing page: ${error.message}`)
toast({
status: "error",
title: publishFailureMsg,
})
await utils.page.readPage.invalidate({ pageId, siteId })
},
})

const handlePublish = () => {
const coercedSiteId = Number(siteId)
const coercedPageId = Number(pageId)
if (coercedSiteId && coercedPageId)
mutate({ pageId: coercedPageId, siteId: coercedSiteId })
}
return (
<Button
variant="solid"
size="sm"
onClick={handlePublish}
isLoading={isLoading}
isDisabled={!currPage.draftBlobId}
>
Publish
</Button>
)
}

const PublishButton = withSuspense(
SuspendablePublishButton,
<Skeleton width={"100%"} height={"100%"} />,
)
export default PublishButton
Original file line number Diff line number Diff line change
@@ -5,9 +5,11 @@ import { Breadcrumb } from "@opengovsg/design-system-react"
import { ADMIN_NAVBAR_HEIGHT } from "~/constants/layouts"
import { useQueryParse } from "~/hooks/useQueryParse"
import { editPageSchema } from "../schema"
import PublishButton from "./PublishButton"

export const SiteEditNavbar = (): JSX.Element => {
const { siteId } = useQueryParse(editPageSchema)
const { siteId, pageId } = useQueryParse(editPageSchema)

return (
<Flex flex="0 0 auto" gridColumn="1/-1">
<Flex
@@ -36,6 +38,12 @@ export const SiteEditNavbar = (): JSX.Element => {
<BreadcrumbLink href="#">Current page</BreadcrumbLink>
</BreadcrumbItem>
</Breadcrumb>

{pageId && siteId && (
<Flex justifyContent={"end"} alignItems={"center"}>
<PublishButton pageId={pageId} siteId={siteId} />
</Flex>
)}
</Flex>
</Flex>
)
11 changes: 11 additions & 0 deletions apps/studio/src/schemas/page.ts
Original file line number Diff line number Diff line change
@@ -13,6 +13,11 @@ export const getEditPageSchema = z.object({
siteId: z.number().min(1),
})

export const getPageSchema = z.object({
pageId: z.number().min(1),
siteId: z.number().min(1),
})

export const reorderBlobSchema = z.object({
pageId: z.number().min(1),
from: z.number().min(0),
@@ -67,3 +72,9 @@ export const createPageSchema = z.object({
// NOTE: implies that top level pages are allowed
folderId: z.number().min(1).optional(),
})

// TODO: siteId should be taken from user's context (not input)
export const publishPageSchema = z.object({
pageId: z.number().min(1),
siteId: z.number().min(1),
})
3 changes: 2 additions & 1 deletion apps/studio/src/server/modules/database/database.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { type DB } from "~prisma/generated/generatedTypes"
import { Kysely, PostgresDialect } from "kysely"
import { PostgresDialect } from "kysely"
import pg from "pg"

import { env } from "~/env.mjs"
import { Kysely } from "./types"

const connectionString = `${env.DATABASE_URL}`

Loading

0 comments on commit f06c139

Please sign in to comment.