Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: opengovsg/isomer
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: e611093a9c9c9fd1dfaab19c78ffbc3d31c7f7aa
Choose a base ref
..
head repository: opengovsg/isomer
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 70ea755a4960e5ea3c7946d17e9b719f05e6a916
Choose a head ref
Showing with 1,859 additions and 588 deletions.
  1. +4 −0 .github/workflows/ci.yml
  2. +3 −1 apps/studio/prisma/constants.ts
  3. +46 −0 apps/studio/prisma/scripts/FileLogger.ts
  4. +71 −0 apps/studio/prisma/scripts/moh-tosp/README.md
  5. +90 −0 apps/studio/prisma/scripts/moh-tosp/backupCollectionById.ts
  6. +165 −0 apps/studio/prisma/scripts/moh-tosp/createCollectionFromLocal.ts
  7. +111 −0 apps/studio/prisma/scripts/moh-tosp/deleteCollectionById.ts
  8. +99 −0 apps/studio/prisma/scripts/moh-tosp/publishDraftCollection.ts
  9. +7 −15 apps/studio/public/assets/css/preview-tw.css
  10. +14 −23 apps/studio/src/components/AppNavbar.tsx
  11. +1 −1 apps/studio/src/components/CmsSidebar/CmsContainer.tsx
  12. +19 −17 apps/studio/src/components/SearchableHeader.tsx
  13. +7 −1 apps/studio/src/features/dashboard/components/DeleteResourceModal/DeleteResourceModal.tsx
  14. +7 −1 apps/studio/src/features/dashboard/components/FolderSettingsModal/FolderSettingsModal.tsx
  15. +1 −0 apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx
  16. +7 −1 ...studio/src/features/editing-experience/components/CreateCollectionModal/CreateCollectionModal.tsx
  17. +7 −1 apps/studio/src/features/editing-experience/components/CreateFolderModal/CreateFolderModal.tsx
  18. +1 −1 apps/studio/src/features/editing-experience/components/CreatePageModal/CreatePageWizardContext.tsx
  19. +1 −0 apps/studio/src/features/editing-experience/components/HeroEditorDrawer.tsx
  20. +3 −1 apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx
  21. +3 −0 apps/studio/src/features/editing-experience/components/PublishButton.tsx
  22. +4 −2 apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx
  23. +2 −0 .../features/editing-experience/components/form-builder/renderers/controls/JsonFormsImageControl.tsx
  24. +4 −1 .../features/editing-experience/components/form-builder/renderers/controls/JsonFormsProseControl.tsx
  25. +2 −1 apps/studio/src/pages/sites/[siteId]/collections/[resourceId].tsx
  26. +2 −1 apps/studio/src/pages/sites/[siteId]/folders/[folderId]/index.tsx
  27. +2 −2 apps/studio/src/pages/sites/[siteId]/pages/[pageId]/settings.tsx
  28. +3 −0 apps/studio/src/pages/sites/[siteId]/settings.tsx
  29. +15 −16 apps/studio/src/server/modules/user/__tests__/user.service.test.ts
  30. +1 −4 apps/studio/src/server/modules/user/user.service.ts
  31. +5 −8 apps/studio/src/stories/Flows/CreateCollectionItemFlow.stories.tsx
  32. +18 −22 apps/studio/src/stories/Flows/CreateNewPageFlow.stories.tsx
  33. +6 −10 apps/studio/src/stories/Page/EditPage/EditArticlePage.stories.tsx
  34. +5 −8 apps/studio/src/stories/Page/EditPage/EditCollectionLink.stories.tsx
  35. +6 −10 apps/studio/src/stories/Page/EditPage/EditContentPage.stories.tsx
  36. +25 −35 apps/studio/src/stories/Page/EditPage/EditHomePage.stories.tsx
  37. +9 −13 apps/studio/src/stories/Page/SitePage.stories.tsx
  38. +6 −1 packages/components/src/engine/index.ts
  39. +12 −7 packages/components/src/engine/metadata.ts
  40. +2 −0 packages/components/src/interfaces/complex/InfoCards.ts
  41. +3 −2 packages/components/src/interfaces/complex/LogoCloud.ts
  42. +51 −0 packages/components/src/interfaces/internal/Vica.ts
  43. +1 −0 packages/components/src/interfaces/internal/index.ts
  44. +3 −0 packages/components/src/templates/next/components/complex/Image/ImageClient.tsx
  45. +1 −1 packages/components/src/templates/next/components/complex/InfoCards/InfoCards.stories.tsx
  46. +42 −7 packages/components/src/templates/next/components/complex/InfoCards/InfoCards.tsx
  47. +22 −0 packages/components/src/templates/next/components/complex/InfoCols/InfoCols.stories.tsx
  48. +71 −33 packages/components/src/templates/next/components/complex/InfoCols/InfoCols.tsx
  49. +34 −3 packages/components/src/templates/next/components/complex/LogoCloud/LogoCloud.stories.tsx
  50. +1 −1 packages/components/src/templates/next/components/complex/LogoCloud/LogoCloud.tsx
  51. +2 −2 packages/components/src/templates/next/components/internal/BlogCard/BlogCard.stories.tsx
  52. +8 −12 packages/components/src/templates/next/components/internal/BlogCard/BlogCard.tsx
  53. +6 −10 packages/components/src/templates/next/components/internal/CollectionCard/CollectionCard.tsx
  54. +4 −5 packages/components/src/templates/next/components/internal/Filter/Filter.stories.tsx
  55. +5 −0 packages/components/src/templates/next/components/internal/FontPreload.tsx
  56. +5 −0 packages/components/src/templates/next/components/internal/Footer/ClientCopyrightYear.tsx
  57. +2 −1 packages/components/src/templates/next/components/internal/Footer/Footer.tsx
  58. +5 −0 ...es/components/src/templates/next/components/internal/GoogleTagManager/GoogleTagManagerPreload.tsx
  59. +1 −0 packages/components/src/templates/next/components/internal/GoogleTagManager/index.ts
  60. +8 −4 packages/components/src/templates/next/components/internal/Navbar/NavItem.tsx
  61. +1 −0 packages/components/src/templates/next/components/internal/Navbar/Navbar.tsx
  62. +39 −0 packages/components/src/templates/next/components/internal/Vica/Vica.tsx
  63. +1 −0 packages/components/src/templates/next/components/internal/Vica/index.ts
  64. +3 −0 packages/components/src/templates/next/components/internal/index.ts
  65. +22 −0 packages/components/src/templates/next/components/internal/utils/PreloadHelper.tsx
  66. +1 −0 packages/components/src/templates/next/components/internal/utils/index.ts
  67. +33 −0 packages/components/src/templates/next/layouts/Collection/Collection.stories.tsx
  68. +4 −5 packages/components/src/templates/next/layouts/Collection/Collection.tsx
  69. +1 −1 packages/components/src/templates/next/layouts/Collection/CollectionClient.tsx
  70. +19 −19 packages/components/src/templates/next/layouts/Collection/CollectionResults.tsx
  71. +0 −256 packages/components/src/templates/next/layouts/Collection/utils.ts
  72. +56 −0 packages/components/src/templates/next/layouts/Collection/utils/__tests__/getCategoryFilter.test.ts
  73. +79 −0 packages/components/src/templates/next/layouts/Collection/utils/__tests__/getTagFilters.test.ts
  74. +95 −0 packages/components/src/templates/next/layouts/Collection/utils/__tests__/getYearFilter.test.ts
  75. +48 −0 packages/components/src/templates/next/layouts/Collection/utils/__tests__/shouldShowDate.test.ts
  76. +3 −0 packages/components/src/templates/next/layouts/Collection/utils/constants.ts
  77. +16 −0 packages/components/src/templates/next/layouts/Collection/utils/getAvailableFilters.ts
  78. +31 −0 packages/components/src/templates/next/layouts/Collection/utils/getCategoryFilter.ts
  79. +76 −0 packages/components/src/templates/next/layouts/Collection/utils/getFilteredItems.ts
  80. +14 −0 packages/components/src/templates/next/layouts/Collection/utils/getPaginatedItems.ts
  81. +53 −0 packages/components/src/templates/next/layouts/Collection/utils/getTagFilters.ts
  82. +51 −0 packages/components/src/templates/next/layouts/Collection/utils/getYearFilter.ts
  83. +5 −0 packages/components/src/templates/next/layouts/Collection/utils/index.ts
  84. +7 −0 packages/components/src/templates/next/layouts/Collection/utils/shouldShowDate.ts
  85. +37 −0 packages/components/src/templates/next/layouts/Collection/utils/updateAppliedFilters.ts
  86. +19 −6 packages/components/src/templates/next/layouts/Skeleton/Skeleton.tsx
  87. +2 −1 packages/components/src/types/site.ts
  88. +3 −3 packages/components/src/utils/__tests__/getParsedData.test.ts
  89. +12 −2 packages/components/src/utils/getParsedDate.ts
  90. +17 −1 tooling/build/scripts/generate-sitemap.js
  91. +0 −1 tooling/build/scripts/publisher.sh
  92. +3 −2 tooling/build/scripts/publishing/index.ts
  93. +3 −2 tooling/build/scripts/publishing/package.json
  94. +5 −3 tooling/build/scripts/publishing/queries.ts
  95. +4 −1 tooling/build/scripts/publishing/types.ts
  96. +17 −0 tooling/build/scripts/publishing/utils/getResourceImage.ts
  97. +8 −0 tooling/template/app/[[...permalink]]/page.tsx
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -90,11 +90,15 @@ jobs:
env:
# Required to allow Datadog to trace Vitest tests
NODE_OPTIONS: -r ${{ env.DD_TRACE_PACKAGE }} --import ${{ env.DD_TRACE_ESM_IMPORT }}
# Set timezone to Singapore to ensure that the tests are run in the correct timezone
TZ: Asia/Singapore
- name: Test Components
run: turbo test-ci:unit --filter=@opengovsg/isomer-components --env-mode=loose
env:
# Required to allow Datadog to trace Vitest tests
NODE_OPTIONS: -r ${{ env.DD_TRACE_PACKAGE }} --import ${{ env.DD_TRACE_ESM_IMPORT }}
# Set timezone to Singapore to ensure that the tests are run in the correct timezone
TZ: Asia/Singapore

end-to-end-tests:
name: End-to-end tests
4 changes: 3 additions & 1 deletion apps/studio/prisma/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const ISOMER_ADMINS = [
"alex",
"alexander",
"jan",
"jiachin",
"sehyun",
@@ -19,4 +19,6 @@ export const ISOMER_MIGRATORS = [
"yongteng",
"huaying",
"weiping",
"sophie",
"felicia",
]
46 changes: 46 additions & 0 deletions apps/studio/prisma/scripts/FileLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import fs from "fs"
import path from "path"

export class FileLogger {
private logFilePath: string

constructor(logFilePath: string) {
this.logFilePath = logFilePath

// Ensure the directory for the log file exists
const logDir = path.dirname(logFilePath)
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true })
}
}

private formatLog(level: string, message: string): string {
const timestamp = new Date().toISOString()
return `[${timestamp}] [${level.toUpperCase()}] ${message}\n`
}

private writeLog(logMessage: string): void {
fs.appendFile(this.logFilePath, logMessage, (err) => {
if (err) {
console.error("Failed to write log:", err)
}
})
}

log(level: string, message: string): void {
const logMessage = this.formatLog(level, message)
this.writeLog(logMessage)
}

info(message: string): void {
this.log("info", message)
}

error(message: string): void {
this.log("error", message)
}

debug(message: string): void {
this.log("debug", message)
}
}
71 changes: 71 additions & 0 deletions apps/studio/prisma/scripts/moh-tosp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# MOH-TOSP Scripts

## Summary

This folder contains developer/ops scripts to update MOH TOSP collection in our DB.

Note: The full steps are covered in [runbook](https://www.notion.so/opengov/Isomer-Next-Runbook-13177dbba78880f4835cdd370c11eef6?pvs=4#16477dbba7888045aadad0fee69c5f79).

To perform the update, this is the overall sequence we will undertake:

1. We will backup the existing collection
2. Load the new collection into DB with all resources in "Draft" state in a different permalink e.g. `cost-financing-new`
3. We will verify the new collection on Studio
4. We will rename the old collection's permalink and swap it with the new collection. E.g. `/cost-financing` becomes `/cost-financing-old`, `/cost-financing-new` is renamed to become `/cost-financing`
5. We will then publish this new draft collection

### Pre-requisites

- Ensure you have the necessary environment variables set up before running these scripts.

### Running the scripts

Use `source .env && npx tsx prisma/scripts/moh-tosp/<script-name>` within `isomer/apps/studio` directory to run any of the scripts.

## Scripts

### 1. `backupCollectionById.ts`

This script takes in 2 input arguments which is the `collectionId` and the `backupDirectory`. Given these params, the script "downloads" the existing collection into the local path of your computer for backup purposes.

### 2. `createCollectionFromLocal.ts`

Pre-requisites:
You will need a folder with the collection to be loaded in the following structure:

```
/content ----> contentDir
/cost-financing ----> collectionName
cost-financing.json ----> indexPageName
```

This script takes in the following arguments:

1. `contentDir`: path to the folder that contains the collection to be loaded into DB
2. `collectionName`: name of the collection to be loaded into DB (e.g. `cost-financing`)
3. `indexPageName`: name of the index page of the collection to be loaded into DB (e.g. `cost-financing`)
4. `indexPageTitle`: name of the index page to show visually (e.g. `Cost financing`)
5. `nameOfNewCollectionToCreate`: This is the name of collection to be created in the DB. This will also become the permalink for the new collection. e.g. `cost-financing-new`
6. `siteId`: ID of the site to create the collection into

### 3. `publishDraftCollection.ts`

This script publishes the draft collection that was created. To do so, it requires the following arguments:

1. `publisherId`: ID of the Studio user who is publishing the collection. This ID can be obtained from the `User` table. Make sure this user has access to the site as well.
2. `collectionId`: ID of the collection to be published. You can check this in the `Resource` table on the `id` column.

### 4. `deleteCollectionById.ts`

**Note: Use this with caution as it is NON-RECOVERABLE**.

This script deletes a collection and its contents. It takes the following arguments:

1. `collectionIdToDelete`: ID of the collection to be deleted. Can be checked in the `Resource` table
2. `siteId`: ID of the site to which the collection belongs to

## Recovery Options

In case that the migration needs to be unrolled back to original state, we can execute the same steps described in "Summary" section using the backup taken.

For full details, refer to the [runbook](https://www.notion.so/opengov/Isomer-Next-Runbook-13177dbba78880f4835cdd370c11eef6?pvs=4#16477dbba7888045aadad0fee69c5f79).
90 changes: 90 additions & 0 deletions apps/studio/prisma/scripts/moh-tosp/backupCollectionById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import fs from "fs/promises" // Use the promise-based version of fs for async/await
import path from "path"

import { db } from "~/server/modules/database"
import { FileLogger } from "../FileLogger"

// Update the logger path if required
const logger = new FileLogger("./backupCollectionById.log")

interface BackupCollectionInput {
resourceId: string
backupDir: string
}

/**
* Backup a collection and its relevant resources to JSON files.
* @param {string} resourceId - ID of the collection resource to back up.
* @param {string} backupDir - Directory to save the backup files.
*/
export async function backupCollection({
resourceId,
backupDir,
}: BackupCollectionInput): Promise<void> {
try {
// Ensure the backup directory exists
await fs.mkdir(backupDir, { recursive: true })

// Fetch the collection resource
const collection = await db
.selectFrom("Resource")
.selectAll()
.where("id", "=", resourceId)
.executeTakeFirst()

if (!collection) {
throw new Error(`Collection with ID ${resourceId} not found.`)
}

// Fetch all child resources
const children = await db
.selectFrom("Resource")
.selectAll()
.where("parentId", "=", resourceId)
.execute()

// Write all the children's published version to the backup directory as JSON files
for (const child of children) {
// fetch the blob
const blob = await db
.selectFrom("Blob")
.select("content")
.innerJoin("Version", "Blob.id", "Version.blobId")
.where("Version.id", "=", child.publishedVersionId)
.executeTakeFirst()

if (!blob) {
throw new Error(
`Published version of child with ID ${child.id} not found.`,
)
}

logger.info(`Writing backup for child with ID ${child.id}`)

// Parse blob content and write to a file
const blobBuffer = blob.content // Assuming blob.content is a buffer
const blobJsonPath = path.join(backupDir, `${child.permalink}.json`)
await fs.writeFile(blobJsonPath, JSON.stringify(blobBuffer, null, 2))
}

logger.info(`Backup completed successfully in directory: ${backupDir}`)
} catch (error) {
if (error instanceof Error) {
logger.error(`Error backing up collection: ${error.message}`)
}
}
}

// Run the backup
// NOTE: TODO: Put in the collection ID to backup
const collectionId = "0"
const backupDirectory = "/Users/XYZ/<your-path>"

await backupCollection({
resourceId: collectionId,
backupDir: backupDirectory,
}).catch((err) => {
if (err instanceof Error) {
logger.error(`Unhandled error: ${err.message}`)
}
})
Loading