Skip to content

Commit

Permalink
Merge pull request #6 from weseek/feat/41-cms-top
Browse files Browse the repository at this point in the history
feat: CMS top page
  • Loading branch information
yuki-takei authored Oct 11, 2023
2 parents 8c7f61a + 8ef71d9 commit fd5c84b
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 0 deletions.
47 changes: 47 additions & 0 deletions apps/app/src/features/cms/client/components/CmsList/CmsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Link from 'next/link';

import { create } from '../../services/cms-namespace';
import { useSWRxCmsNamespaces } from '../../stores/cms-namespace';

export const CmsList = (): JSX.Element => {
const { data } = useSWRxCmsNamespaces();

if (data == null) { // data should be not null by `suspense: true`
return <></>;
}

return (
<table className="table table-bordered grw-duplicated-paths-table">
<thead>
<tr>
<th className="col-4">namespace</th>
<th>desc</th>
</tr>
</thead>
<tbody className="overflow-auto">
{data.map((cmsNamespace) => {
const { namespace, desc } = cmsNamespace;
return (
<tr key={namespace}>
<td>
<Link href={`/_cms/${namespace}`} prefetch={false}>
{namespace}
</Link>
</td>
<td>
{desc}
</td>
</tr>
);
})}
<tr>
<td colSpan={2} className="text-center">
<button type="button" className="btn btn-outline-secondary">
<span className="icon icon-plus" /> Add
</button>
</td>
</tr>
</tbody>
</table>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CmsList';
11 changes: 11 additions & 0 deletions apps/app/src/features/cms/client/services/cms-namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { apiv3Post } from '~/client/util/apiv3-client';

import { ICmsNamespace } from '../../interfaces';

export const create = async(namespace: string, desc?: string): Promise<void> => {
const newNamespace: ICmsNamespace = {
namespace,
desc,
};
await apiv3Post('/cms/namespace', { data: newNamespace });
};
25 changes: 25 additions & 0 deletions apps/app/src/features/cms/client/stores/cms-namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import useSWR, { type SWRConfiguration, type SWRResponse } from 'swr';

import { apiv3Get } from '~/client/util/apiv3-client';

import type { ICmsNamespace } from '../../interfaces';

type ListCmsNamespaceResults = {
data: ICmsNamespace[],
}

export const useSWRxCmsNamespaces = (config?: SWRConfiguration): SWRResponse<ICmsNamespace[], Error> => {
return useSWR(
'/cms/namespace',
async(endpoint) => {
try {
const res = await apiv3Get<ListCmsNamespaceResults>(endpoint);
return res.data.data;
}
catch (err) {
throw new Error(err);
}
},
config,
);
};
7 changes: 7 additions & 0 deletions apps/app/src/features/cms/interfaces/cms-namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type ICmsNamespaceAttribute = Map<string, any>;
export type ICmsNamespace = {
namespace: string,
desc?: string,
attributes?: ICmsNamespaceAttribute[],
meta?: Map<string, any>,
};
1 change: 1 addition & 0 deletions apps/app/src/features/cms/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cms-namespace';
Empty file.
22 changes: 22 additions & 0 deletions apps/app/src/features/cms/server/models/cms-namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
Schema, type Model, type Document,
} from 'mongoose';

import { getOrCreateModel } from '~/server/util/mongoose-utils';

import type { ICmsNamespace } from '../../interfaces';

export interface ICmsNamespaceDocument extends ICmsNamespace, Document {
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ICmsNamespaceModel extends Model<ICmsNamespaceDocument> {
}

const cmsNamespaceSchema = new Schema<ICmsNamespaceDocument, ICmsNamespace>({
namespace: { type: String, required: true, unique: true },
desc: { type: String },
attributes: [Map],
meta: Map,
});

export const CmsNamespace = getOrCreateModel<ICmsNamespaceDocument, ICmsNamespaceModel>('CmsNamespace', cmsNamespaceSchema);
1 change: 1 addition & 0 deletions apps/app/src/features/cms/server/models/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './cms-namespace';
31 changes: 31 additions & 0 deletions apps/app/src/features/cms/server/routes/apiv3/cms-namespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import express, { Request, Router } from 'express';

import Crowi from '~/server/crowi';
import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';

import { CmsNamespace } from '../../models';


/*
* Validators
*/
const validator = {
};

module.exports = (crowi: Crowi): Router => {
const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);

const router = express.Router();

router.get('/', loginRequiredStrictly, async(req: Request, res: ApiV3Response) => {
try {
const data = await CmsNamespace.find({});
return res.apiv3({ data });
}
catch (err) {
return res.apiv3Err(err);
}
});

return router;
};
12 changes: 12 additions & 0 deletions apps/app/src/features/cms/server/routes/apiv3/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import express, { Router } from 'express';

import Crowi from '~/server/crowi';


module.exports = (crowi: Crowi): Router => {
const router = express.Router();

router.use('/namespace', require('./cms-namespace')(crowi));

return router;
};
156 changes: 156 additions & 0 deletions apps/app/src/pages/_cms/index.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React, { ReactNode } from 'react';

import type { IUser, IUserHasId } from '@growi/core';
import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import dynamic from 'next/dynamic';
import Head from 'next/head';

import { GrowiSubNavigation } from '~/components/Navbar/GrowiSubNavigation';
import type { CrowiRequest } from '~/interfaces/crowi-request';
import { useCurrentPageId } from '~/stores/page';
import { useDrawerMode } from '~/stores/ui';

import { BasicLayout } from '../../components/Layout/BasicLayout';
import {
useCurrentUser, useCurrentPathname, useGrowiCloudUri,
useIsSearchServiceConfigured, useIsSearchServiceReachable,
useIsSearchScopeChildrenAsDefault, useIsSearchPage, useShowPageLimitationXL,
} from '../../stores/context';
import type { NextPageWithLayout } from '../_app.page';
import type { CommonProps } from '../utils/commons';
import {
getServerSideCommonProps, getNextI18NextConfig, generateCustomTitleForPage, useInitSidebarConfig,
} from '../utils/commons';

const CmsList = dynamic(() => import('~/features/cms/client/components/CmsList').then(mod => mod.CmsList), { ssr: false });

type Props = CommonProps & {
currentUser: IUser,
isSearchServiceConfigured: boolean,
isSearchServiceReachable: boolean,
isSearchScopeChildrenAsDefault: boolean,
showPageLimitationXL: number,
};

const CmsPage: NextPageWithLayout<CommonProps> = (props: Props) => {
useCurrentUser(props.currentUser ?? null);

useGrowiCloudUri(props.growiCloudUri);

useIsSearchServiceConfigured(props.isSearchServiceConfigured);
useIsSearchServiceReachable(props.isSearchServiceReachable);
useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);

useIsSearchPage(false);
useCurrentPageId(null);
useCurrentPathname('/_cms');

// init sidebar config with UserUISettings and sidebarConfig
useInitSidebarConfig(props.sidebarConfig, props.userUISettings);

useShowPageLimitationXL(props.showPageLimitationXL);

const { data: isDrawerMode } = useDrawerMode();

const title = generateCustomTitleForPage(props, 'GROWI CMS Manager');

return (
<>
<Head>
<title>{title}</title>
</Head>
<div className="dynamic-layout-root">
<header className="py-0 position-relative">
<GrowiSubNavigation
pagePath="/GROWI CMS Manager"
showDrawerToggler={isDrawerMode}
isTagLabelsDisabled
isDrawerMode={isDrawerMode}
additionalClasses={['container-fluid']}
/>
</header>

<div className="content-main container-lg grw-container-convertible mb-5 pb-5">
<CmsList />
</div>

<div id="grw-fav-sticky-trigger" className="sticky-top"></div>
</div>
</>
);
};

type LayoutProps = Props & {
children?: ReactNode,
}

const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
// init sidebar config with UserUISettings and sidebarConfig
useInitSidebarConfig(props.sidebarConfig, props.userUISettings);

return <BasicLayout>{children}</BasicLayout>;
};

CmsPage.getLayout = function getLayout(page) {
return (
<>
<Layout {...page.props}>
{page}
</Layout>
</>
);
};

function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
const req: CrowiRequest = context.req as CrowiRequest;
const { crowi } = req;
const {
searchService, configManager,
} = crowi;

props.isSearchServiceConfigured = searchService.isConfigured;
props.isSearchServiceReachable = searchService.isReachable;
props.isSearchScopeChildrenAsDefault = configManager.getConfig('crowi', 'customize:isSearchScopeChildrenAsDefault');
props.showPageLimitationXL = crowi.configManager.getConfig('crowi', 'customize:showPageLimitationXL');

props.sidebarConfig = {
isSidebarDrawerMode: configManager.getConfig('crowi', 'customize:isSidebarDrawerMode'),
isSidebarClosedAtDockMode: configManager.getConfig('crowi', 'customize:isSidebarClosedAtDockMode'),
};

}

/**
* for Server Side Translations
* @param context
* @param props
* @param namespacesRequired
*/
async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
props._nextI18Next = nextI18NextConfig._nextI18Next;
}

export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
const req = context.req as CrowiRequest<IUserHasId & any>;
const { user } = req;
const result = await getServerSideCommonProps(context);

if (!('props' in result)) {
throw new Error('invalid getSSP result');
}
const props: Props = result.props as Props;

if (user != null) {
props.currentUser = user.toObject();
}
injectServerConfigurations(context, props);
await injectNextI18NextConfigurations(context, props, ['translation']);

return {
props,
};
};

export default CmsPage;
1 change: 1 addition & 0 deletions apps/app/src/server/routes/apiv3/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ module.exports = (crowi, app) => {
router.use('/bookmark-folder', require('./bookmark-folder')(crowi));
router.use('/questionnaire', require('~/features/questionnaire/server/routes/apiv3/questionnaire')(crowi));
router.use('/templates', require('~/features/templates/server/routes/apiv3')(crowi));
router.use('/cms', require('~/features/cms/server/routes/apiv3')(crowi));

return [router, routerForAdmin, routerForAuth];
};

0 comments on commit fd5c84b

Please sign in to comment.