diff --git a/packages/studiocms/src/hooks/config-setup.ts b/packages/studiocms/src/hooks/config-setup.ts index cfc9ed0a3..eeca4ca2f 100644 --- a/packages/studiocms/src/hooks/config-setup.ts +++ b/packages/studiocms/src/hooks/config-setup.ts @@ -96,6 +96,7 @@ export const configSetup = defineUtility('astro:config:setup')( { name: 'StudioCMS (Default)', identifier: 'studiocms', + pageTypes: [{ label: 'Normal (StudioCMS)', identifier: 'studiocms' }], }, ]; diff --git a/packages/studiocms_core/src/schemas/plugins/index.ts b/packages/studiocms_core/src/schemas/plugins/index.ts index 5b86a0344..038bae5b9 100644 --- a/packages/studiocms_core/src/schemas/plugins/index.ts +++ b/packages/studiocms_core/src/schemas/plugins/index.ts @@ -101,6 +101,19 @@ export const StudioCMSPluginSchema = z.object({ * Label that is shown in the select input */ label: z.string(), + /** + * Identifier that is saved in the database + * @example + * // Single page type per plugin + * 'studiocms' + * '@studiocms/blog' + * // Multiple page types per plugin (Use unique identifiers for each type to avoid conflicts) + * '@mystudiocms/plugin:pageType1' + * '@mystudiocms/plugin:pageType2' + * '@mystudiocms/plugin:pageType3' + * '@mystudiocms/plugin:pageType4' + */ + identifier: z.string(), /** * Description that is shown below the "Page Content" header if this type is selected */ diff --git a/packages/studiocms_core/src/sdk-utils/StudioCMSSDK.ts b/packages/studiocms_core/src/sdk-utils/StudioCMSSDK.ts index 03462de5f..b7a03e1fe 100644 --- a/packages/studiocms_core/src/sdk-utils/StudioCMSSDK.ts +++ b/packages/studiocms_core/src/sdk-utils/StudioCMSSDK.ts @@ -20,6 +20,7 @@ import type { CombinedRank, CombinedUserData, DeletionResponse, + FolderListItem, FolderNode, MultiPageInsert, PageContentReturnId, @@ -143,6 +144,20 @@ export class StudioCMSSDK { return this.generateFolderTree(currentFolders); } + /** + * Gets the available folders from the database. + * + * @returns A promise that resolves to an array of folder list items. + */ + public async getAvailableFolders(): Promise { + const folders: FolderListItem[] = []; + const currentFolders = await this.db.select().from(tsPageFolderStructure); + for (const { id, name } of currentFolders) { + folders.push({ id, name }); + } + return folders; + } + /** * Finds a node in the tree that matches the given URL path. * @param tree - The root of the folder tree. diff --git a/packages/studiocms_core/src/sdk-utils/StudioCMSVirtualCache.ts b/packages/studiocms_core/src/sdk-utils/StudioCMSVirtualCache.ts index c0122b93e..586092901 100644 --- a/packages/studiocms_core/src/sdk-utils/StudioCMSVirtualCache.ts +++ b/packages/studiocms_core/src/sdk-utils/StudioCMSVirtualCache.ts @@ -3,6 +3,8 @@ import { StudioCMSCacheError } from './errors'; import type { BaseCacheObject, CombinedPageData, + FolderListCacheObject, + FolderListItem, FolderNode, FolderTreeCacheObject, PageDataCacheObject, @@ -32,6 +34,7 @@ export class StudioCMSVirtualCache { private readonly VersionMapID: string = '__StudioCMS_Latest_Version'; private readonly FolderTreeMapID: string = '__StudioCMS_Folder_Tree'; private readonly PageFolderTreeMapID: string = '__StudioCMS_Page_Folder_Tree'; + private readonly FolderListMapID: string = '__StudioCMS_Folder_List'; private readonly StudioCMSPkgId: string = 'studiocms'; private readonly CMSSiteConfigId = CMSSiteConfigId; private readonly versionCacheLifetime = versionCacheLifetime; @@ -44,6 +47,7 @@ export class StudioCMSVirtualCache { private version = new Map(); private folderTree = new Map(); private pageFolderTree = new Map(); + private FolderList = new Map(); constructor(cacheConfig: ProcessedCacheConfig, sdkCore: STUDIOCMS_SDK) { this.cacheConfig = cacheConfig; @@ -140,8 +144,76 @@ export class StudioCMSVirtualCache { }; } + private folderListReturn(data: FolderListItem[]): FolderListCacheObject { + return { + data, + lastCacheUpdate: new Date(), + }; + } + // Folder Tree Utils + public async getFolderList(): Promise { + try { + if (!this.isEnabled()) { + const folderList = await this.sdk.getAvailableFolders(); + + if (!folderList) { + throw new StudioCMSCacheError('Folder list not found in database'); + } + + return this.folderListReturn(folderList); + } + + const list = this.FolderList.get(this.FolderListMapID); + + if (!list || this.isCacheExpired(list)) { + const folderList = await this.sdk.getAvailableFolders(); + + if (!folderList) { + throw new StudioCMSCacheError('Folder list not found in database'); + } + + this.FolderList.set(this.FolderListMapID, this.folderListReturn(folderList)); + + return this.folderListReturn(folderList); + } + + return list; + } catch (error) { + throw new StudioCMSCacheError('Error fetching folder list'); + } + } + + public async updateFolderList(): Promise { + try { + const folderList = await this.sdk.getAvailableFolders(); + + if (!this.isEnabled()) { + return this.folderListReturn(folderList); + } + + this.FolderList.set(this.FolderListMapID, this.folderListReturn(folderList)); + + return this.folderListReturn(folderList); + } catch (error) { + throw new StudioCMSCacheError('Error updating folder list'); + } + } + + public clearFolderList(): void { + // Check if caching is disabled + if (!this.isEnabled()) { + return; + } + + // Clear the folder list cache + this.FolderList.clear(); + + // Return void + return; + } + /** * Retrieves the folder tree from the cache or the database. * @@ -824,6 +896,7 @@ export class StudioCMSVirtualCache { latestVersion: async () => await this.getVersion(), folderTree: async () => await this.getFolderTree(), pageFolderTree: async () => await this.getPageFolderTree(), + folderList: async () => await this.getFolderList(), }, CLEAR: { page: { @@ -833,6 +906,7 @@ export class StudioCMSVirtualCache { pages: () => this.clearAllPages(), latestVersion: () => this.clearVersion(), folderTree: () => this.clearFolderTree(), + folderList: () => this.clearFolderList(), }, UPDATE: { page: { @@ -849,6 +923,7 @@ export class StudioCMSVirtualCache { siteConfig: async (data: SiteConfig) => await this.updateSiteConfig(data), latestVersion: async () => await this.updateVersion(), folderTree: async () => await this.updateFolderTree(), + folderList: async () => await this.updateFolderList(), }, }; } diff --git a/packages/studiocms_core/src/sdk-utils/types/index.ts b/packages/studiocms_core/src/sdk-utils/types/index.ts index ef151291c..af658941e 100644 --- a/packages/studiocms_core/src/sdk-utils/types/index.ts +++ b/packages/studiocms_core/src/sdk-utils/types/index.ts @@ -92,6 +92,11 @@ export interface FolderNode { children: FolderNode[]; } +export interface FolderListItem { + id: string; + name: string; +} + export type AstroDBVirtualModule = typeof import('astro:db'); // ../../schemas/config/sdk.ts @@ -171,10 +176,32 @@ export interface VersionCacheObject extends BaseCacheObject { version: string; } +/** + * Represents a cache object for folder tree data. + * Extends the BaseCacheObject interface. + * + * @interface FolderTreeCacheObject + * @extends {BaseCacheObject} + * + * @property {FolderNode[]} data - The folder tree data to be cached. + */ export interface FolderTreeCacheObject extends BaseCacheObject { data: FolderNode[]; } +/** + * Represents a cache object for folder list data. + * Extends the BaseCacheObject interface. + * + * @interface FolderListCacheObject + * @extends {BaseCacheObject} + * + * @property {FolderListItem[]} data - The folder list data to be cached. + */ +export interface FolderListCacheObject extends BaseCacheObject { + data: FolderListItem[]; +} + /** * Represents a cache object that stores pages and site configuration data. */ diff --git a/packages/studiocms_dashboard/src/components/islands/content-mgmt/Edit.astro b/packages/studiocms_dashboard/src/components/islands/content-mgmt/Edit.astro index aaa1370ce..15e9a064a 100644 --- a/packages/studiocms_dashboard/src/components/islands/content-mgmt/Edit.astro +++ b/packages/studiocms_dashboard/src/components/islands/content-mgmt/Edit.astro @@ -1,17 +1,43 @@ --- import '../../../styles/tiny-mde.css'; import { StudioCMSRoutes } from 'studiocms:lib'; +import pluginsList from 'studiocms:plugins'; +import { studioCMS_SDK_Cache } from 'studiocms:sdk/cache'; import { Input, Select } from '@studiocms/ui/components'; import TinyMDE from '../../component-scripts/TinyMDE.astro'; -const pageTypeOptions = [{ label: 'Normal (StudioCMS)', value: 'studiocms' }]; +type PluginList = { + label: string; + value: string; +}[]; + +const { data: folderList } = await studioCMS_SDK_Cache.GET.folderList(); + +const pageTypeOptions = pluginsList.flatMap(({ pageTypes }) => { + const pageTypeOutput: PluginList = []; + + if (!pageTypes) { + return pageTypeOutput; + } + + for (const { label, identifier } of pageTypes) { + pageTypeOutput.push({ label, value: identifier }); + } + + return pageTypeOutput; +}); + +const parentFolders = folderList.map(({ id: value, name: label }) => ({ label, value })); + +const parentFolderOptions = [{ label: 'None', value: 'null' }, ...parentFolders]; + const trueFalse = [ { label: 'Yes', value: 'true' }, { label: 'No', value: 'false' }, ]; + const categoriesOptions = [{ label: 'None', value: 'null' }]; const tagsOptions = [{ label: 'None', value: 'null' }]; -const parentFolderOptions = [{ label: 'None', value: 'null' }]; ---
@@ -36,6 +62,7 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; label="Show in Navigation" name="show-in-nav" isRequired + defaultValue='false' fullWidth options={trueFalse} /> @@ -47,13 +74,13 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }];
- +
- +

Page Content

@@ -88,7 +115,6 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; // get the elements const editPageContainer = document.getElementById('edit-page-container'); - const editPageForm = document.getElementById('edit-page-form'); const pageTitleInput = editPageForm.querySelector('input[name="page-title"]'); @@ -99,7 +125,6 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; const showInNavSelect = editPageForm.querySelector('select[name="show-in-nav"]'); const showInNavSelectValue = editPageForm.querySelector(`#show-in-nav-value-span`); const pageHeroImageInput = editPageForm.querySelector('input[name="page-hero-image"]'); - const categoriesSelect = editPageForm.querySelector('select[name="categories"]'); const categoriesSelectValue = editPageForm.querySelector(`#categories-value-span`); const tagsSelect = editPageForm.querySelector('select[name="tags"]'); @@ -111,6 +136,8 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; const showContributorsSelect = editPageForm.querySelector('select[name="show-contributors"]'); const showContributorsSelectValue = editPageForm.querySelector(`#show-contributors-value-span`); + const parentFolderSelectContainer = editPageForm.querySelector('#parent-folder-container'); + const pageContentTextarea = document.getElementById('page-content') const editorToolbar = document.getElementById('editor-toolbar'); @@ -157,6 +184,10 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; pageDescriptionInput.value = pageData.data.description; pageHeroImageInput.value = pageData.data.heroImage; + if (pageData.data.slug === 'index' || pageData.data.slug === 'about') { + parentFolderSelectContainer.classList.add('disabled'); + } + setSelectValue(pageTypeSelect, pageTypeSelectValue, pageData.data.package, getPageTypeOption(pageData.data.package)); setSelectValue(showInNavSelect, showInNavSelectValue, pageData.data.showOnNav, pageData.data.showOnNav === true ? 'Yes' : 'No'); setSelectValue(showAuthorSelect, showAuthorSelectValue, pageData.data.showAuthor, pageData.data.showAuthor === true ? 'Yes' : 'No'); @@ -268,6 +299,26 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; pageContentTextarea.textContent = e.content; }); + tinyMDE.addEventListener("drop", function (event) { + let formData = new FormData(); + + // You can add use event.dataTransfer.items or event.dataTransfer.files + // to build the form data object: + for (let i = 0; i < event.dataTransfer.items.length; i++) { + if (event.dataTransfer.items[i].kind === "file") { + let file = event.dataTransfer.items[i].getAsFile(); + formData.append("image", file); + } + } + + // Call your API endpoint that accepts "Content-Type": "multipart/form-data" + // requests and responds with the image names and URL-s. + // + // Now you can add Markdown images like so: + // editor.paste(`![${imageName}](${imageUrl})`); + alert("Image upload coming soon!"); + }); + // Set default page content in the textarea pageContentTextarea.textContent = pageData.data.defaultContent.content; @@ -279,7 +330,7 @@ const parentFolderOptions = [{ label: 'None', value: 'null' }]; } listener(); - +