Просмотр исходного кода

Merge pull request #26 from weseek/support/plugincards

support: Upate Plugin cards
ryoji-s 3 лет назад
Родитель
Сommit
2e383a58a0

+ 24 - 30
packages/app/src/components/Admin/PluginsExtension/PluginCard.tsx

@@ -1,14 +1,31 @@
-// import { faCircleArrowDown, faCircleCheck } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { useCallback, useState } from 'react';
 
-import Image from 'next/image';
-import Link from 'next/link';
+import { useState } from 'react';
 
-import { SearchResultItem } from '~/models/SearchResultItem';
+import Link from 'next/link';
 
 import styles from './PluginCard.module.scss';
 
+const PluginCardButton = ({ onChange }: any): JSX.Element => {
+  const [isEnabled, setIsEnabled] = useState(true);
+
+  return (
+    <div className={`${styles.plugin_card}`}>
+      <div className="switch">
+        <label className="switch__label">
+          <input
+            type="checkbox"
+            className="switch__input"
+            onChange={() => setIsEnabled(!isEnabled)}
+            checked={isEnabled}
+          />
+          <span className="switch__content"></span>
+          <span className="switch__circle"></span>
+        </label>
+      </div>
+    </div>
+  );
+};
+
 
 type Props = {
   name: string,
@@ -20,11 +37,6 @@ export const PluginCard = (props: Props): JSX.Element => {
   const {
     name, url, description,
   } = props;
-  // const [isEnabled, setIsEnabled] = useState(true);
-
-  // const checkboxHandler = useCallback(() => {
-  //   setIsEnabled(false);
-  // }, []);
 
   return (
     <div className="card shadow border-0" key={name}>
@@ -37,25 +49,7 @@ export const PluginCard = (props: Props): JSX.Element => {
             <p className="card-text text-muted">{description}</p>
           </div>
           <div className='col-3'>
-            <div className={`${styles.plugin_card}`}>
-              <div className="switch">
-                <label className="switch__label">
-                  <input type="checkbox" className="switch__input" checked/>
-                  <span className="switch__content"></span>
-                  <span className="switch__circle"></span>
-                </label>
-              </div>
-            </div>
-            {/* <div className="custom-control custom-switch custom-switch-lg custom-switch-slack">
-              <input
-                type="checkbox"
-                className="custom-control-input border-0"
-                checked={isEnabled}
-                onChange={checkboxHandler}
-              />
-              <label className="custom-control-label align-center"></label>
-            </div> */}
-            {/* <Image className="mx-auto" alt="GitHub avator image" src={owner.avatar_url} width={250} height={250} /> */}
+            <PluginCardButton />
           </div>
         </div>
         <div className="row">

+ 0 - 34
packages/app/src/components/Admin/PluginsExtension/PluginInstallerForm.tsx

@@ -1,17 +1,9 @@
 import React, { useCallback } from 'react';
 
-import { useTranslation } from 'react-i18next';
-
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
-
-import AdminInstallButtonRow from '../Common/AdminUpdateButtonRow';
-// TODO: error notification (toast, loggerFactory)
 // TODO: i18n
-
 export const PluginInstallerForm = (): JSX.Element => {
-  // const { t } = useTranslation('admin');
-
   const submitHandler = useCallback(async(e) => {
     e.preventDefault();
 
@@ -47,39 +39,13 @@ export const PluginInstallerForm = (): JSX.Element => {
           <input
             className="form-control"
             type="text"
-            // defaultValue={adminAppContainer.state.title || ''}
             name="pluginInstallerForm[url]"
             placeholder="https://github.com/weseek/growi-plugin-lsx"
             required
           />
           <p className="form-text text-muted">You can install plugins by inputting the GitHub URL.</p>
-          {/* <p className="form-text text-muted">{t('admin:app_setting.sitename_change')}</p> */}
-        </div>
-      </div>
-      {/* <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">branch</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghBranch]"
-            placeholder="main"
-          />
-          <p className="form-text text-muted">branch name</p>
         </div>
       </div>
-      <div className='form-group row'>
-        <label className="text-left text-md-right col-md-3 col-form-label">tag</label>
-        <div className="col-md-6">
-          <input
-            className="form-control"
-            type="text"
-            name="pluginInstallerForm[ghTag]"
-            placeholder="tags"
-          />
-          <p className="form-text text-muted">tag name</p>
-        </div>
-      </div> */}
 
       <div className="row my-3">
         <div className="mx-auto">

+ 16 - 27
packages/app/src/components/Admin/PluginsExtension/PluginsExtensionPageContents.tsx

@@ -1,21 +1,18 @@
 import React from 'react';
 
-import { SearchResultItem } from '~/models/SearchResultItem';
-import { useInstalledPlugins } from '~/stores/useInstalledPlugins';
+import { usePluginEntries } from '~/stores/context';
 
 import Loading from './Loading';
 import { PluginCard } from './PluginCard';
 import { PluginInstallerForm } from './PluginInstallerForm';
-
-
 // TODO: i18n
 
 export const PluginsExtensionPageContents = (): JSX.Element => {
-  // const { data, error } = useInstalledPlugins();
+  const { data, mutate } = usePluginEntries();
 
-  // if (data == null) {
-  //   return <Loading />;
-  // }
+  if (data == null) {
+    return <Loading />;
+  }
 
   return (
     <div>
@@ -29,26 +26,18 @@ export const PluginsExtensionPageContents = (): JSX.Element => {
 
       <div className="row mb-5">
         <div className="col-lg-12">
-          <h2 className="admin-setting-header">Plugins</h2>
+          <h2 className="admin-setting-header">Plugins
+            <button type="button" className="btn btn-sm ml-auto grw-btn-reload" onClick={() => mutate()}>
+              <i className="icon icon-reload"></i>
+            </button>
+          </h2>
           <div className="d-grid gap-5">
-            <PluginCard
-              name={'growi-plugin-templates-for-office'}
-              url={'https://github.com/weseek/growi-plugin-templates-for-office'}
-              description={'GROWI markdown templates for office.'}
-            />
-            {/* <PluginCard
-              name={'growi-plugin-theme-welcome-to-fumiya-room'}
-              url={'https://github.com/weseek/growi-plugin-theme-welcome-to-fumiya-room'}
-              description={'Welcome to fumiya\'s room! This is very very "latest" design...'}
-            /> */}
-            <PluginCard
-              name={'growi-plugin-copy-code-to-clipboard'}
-              url={'https://github.com/weseek/growi-plugin-copy-code-to-clipboard'}
-              description={'Add copy button on code blocks.'}
-            />
-            {/* {data?.items.map((item: SearchResultItem) => {
-              return <PluginCard key={item.name} {...item} />;
-            })} */}
+            { data.map((item) => {
+              const pluginName = item[0].meta.name;
+              const pluginUrl = item[0].origin.url;
+              const pluginDiscription = item[0].meta.desc;
+              return <PluginCard key={pluginName} name={pluginName} url={pluginUrl} description={pluginDiscription} />;
+            })}
           </div>
         </div>
       </div>

+ 5 - 1
packages/app/src/models/SearchResult.ts

@@ -1,7 +1,11 @@
-import { SearchResultItem } from './SearchResultItem';
+import { SearchResultItem, PluginItem } from './SearchResultItem';
 
 export type SearchResult = {
   total_count: number,
   imcomplete_results: boolean,
   items: SearchResultItem[];
 }
+
+export type PluginResult = {
+  items: PluginItem[];
+}

+ 6 - 0
packages/app/src/models/SearchResultItem.ts

@@ -13,3 +13,9 @@ export type SearchResultItem = {
   homepage: string,
   stargazersCount: number,
 }
+
+export type PluginItem = {
+  name: string,
+  url: string,
+  description: string,
+}

+ 17 - 1
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 
 import { isClient, objectIdUtils } from '@growi/core';
+import mongoose from 'mongoose';
 import {
   NextPage, GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -28,12 +29,14 @@ import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityConta
 import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
 import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
 import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
 import { SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
+import { GrowiPlugin } from '~/interfaces/plugin';
 import ConfigLoader from '~/server/service/config-loader';
 import {
   useCurrentUser, /* useSearchServiceConfigured, */ useIsAclEnabled, useIsMailerSetup, useIsSearchServiceReachable, useSiteUrl,
-  useAuditLogEnabled, useAuditLogAvailableActions,
+  useAuditLogEnabled, useAuditLogAvailableActions, usePluginEntries,
 } from '~/stores/context';
 import { useIsMaintenanceMode } from '~/stores/maintenanceMode';
 
@@ -81,6 +84,8 @@ type Props = CommonProps & {
   auditLogEnabled: boolean,
   auditLogAvailableActions: SupportedActionType[],
 
+  pluginManifestEntries: any,
+
   siteUrl: string,
 };
 
@@ -208,6 +213,7 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
   useAuditLogEnabled(props.auditLogEnabled);
   useAuditLogAvailableActions(props.auditLogAvailableActions);
+  usePluginEntries(props.pluginManifestEntries);
 
   const injectableContainers: Container<any>[] = [];
 
@@ -315,6 +321,15 @@ async function injectNextI18NextConfigurations(context: GetServerSidePropsContex
   props._nextI18Next = nextI18NextConfig._nextI18Next;
 }
 
+async function getPluginProps(ctx, props): Promise<any> {
+  // const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx);
+
+  const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+  const growiPlugins = await GrowiPlugin.find({ isEnabled: true });
+  const pluginManifestEntries: GrowiPluginManifestEntries = await ActivatePluginService.retrievePluginManifests(growiPlugins);
+  props.pluginManifestEntries = JSON.parse(JSON.stringify(pluginManifestEntries));
+}
+
 export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
   const req: CrowiRequest = context.req as CrowiRequest;
 
@@ -334,6 +349,7 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
 
   injectServerConfigurations(context, props);
   await injectNextI18NextConfigurations(context, props, ['admin']);
+  await getPluginProps(context, props);
 
   return {
     props,

+ 16 - 1
packages/app/src/server/routes/apiv3/plugins-extention.ts

@@ -1,4 +1,5 @@
 import express, { Request } from 'express';
+import { PromiseProvider } from 'mongoose';
 
 import Crowi from '../../crowi';
 
@@ -10,6 +11,21 @@ module.exports = (crowi: Crowi) => {
   const router = express.Router();
   const { pluginService } = crowi;
 
+  // router.get('/', async(req, res) => {
+  //   if (pluginService == null) {
+  //     // return res.apiv3Err(400);
+  //     return; // console.log('err');
+  //   }
+
+  //   try {
+  //     const res = await pluginService.getPlugins();
+  //     return res.apiv3(res);
+  //   }
+  //   catch (err) {
+  //     // return res.apiv3Err(err, 400);
+  //   }
+  // });
+
   router.post('/', async(req: PluginInstallerFormRequest, res: ApiV3Response) => {
     if (pluginService == null) {
       return res.apiv3Err(400);
@@ -20,7 +36,6 @@ module.exports = (crowi: Crowi) => {
       return res.apiv3({});
     }
     catch (err) {
-      // TODO: error handling
       return res.apiv3Err(err, 400);
     }
   });

+ 12 - 2
packages/app/src/server/service/plugin.ts

@@ -1,10 +1,10 @@
 import { execSync } from 'child_process';
-import fs from 'fs';
 import path from 'path';
 
 import mongoose from 'mongoose';
 
-import { GrowiPlugin, GrowiPluginMeta, GrowiPluginOrigin } from '~/interfaces/plugin';
+import { ActivatePluginService, GrowiPluginManifestEntries } from '~/client/services/activate-plugin';
+import { GrowiPlugin, GrowiPluginOrigin } from '~/interfaces/plugin';
 import loggerFactory from '~/utils/logger';
 import { resolveFromRoot } from '~/utils/project-dir-utils';
 
@@ -61,6 +61,7 @@ export class PluginService {
     const plugins = await PluginService.detectPlugins(origin, installedPath);
     await this.savePluginMetaData(plugins);
 
+
     return;
   }
 
@@ -85,6 +86,15 @@ export class PluginService {
     await GrowiPlugin.insertMany(plugins);
   }
 
+  async getPlugins(): Promise<any> {
+    // const initialProps: DocumentInitialProps = await Document.getInitialProps(ctx);
+
+    const GrowiPlugin = mongoose.model<GrowiPlugin>('GrowiPlugin');
+    const growiPlugins = await GrowiPlugin.find({ isEnabled: true });
+    const pluginManifestEntries: GrowiPluginManifestEntries = await ActivatePluginService.retrievePluginManifests(growiPlugins);
+    return JSON.parse(JSON.stringify(pluginManifestEntries));
+  }
+
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   static async detectPlugins(origin: GrowiPluginOrigin, installedPath: string, parentPackageJson?: any): Promise<GrowiPlugin[]> {
     const packageJsonPath = path.resolve(pluginStoringPath, installedPath, 'package.json');

+ 4 - 0
packages/app/src/stores/context.tsx

@@ -178,6 +178,10 @@ export const useIsMailerSetup = (initialData?: boolean): SWRResponse<boolean, an
   return useStaticSWR('isMailerSetup', initialData);
 };
 
+export const usePluginEntries = (initialData?: any): SWRResponse<any, any> => {
+  return useStaticSWR('pluginEntries', initialData);
+};
+
 export const useIsSearchScopeChildrenAsDefault = (initialData?: boolean) : SWRResponse<boolean, Error> => {
   return useStaticSWR<boolean, Error>('isSearchScopeChildrenAsDefault', initialData);
 };

+ 62 - 0
packages/app/src/stores/plugin-test.tsx

@@ -0,0 +1,62 @@
+import { useCallback } from 'react';
+
+import {
+  HasObjectId,
+  IAttachment, Nullable, SWRResponseWithUtils, withUtils,
+} from '@growi/core';
+import useSWR, { SWRResponse } from 'swr';
+
+import { apiPost } from '~/client/util/apiv1-client';
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { IResAttachmentList } from '~/interfaces/attachment';
+
+import { PluginResult } from '../models/SearchResult';
+
+
+const pluginsFetcher = () => {
+  return async() => {
+    const reqUrl = '/plugins-extention';
+    const data = await fetch(reqUrl).then(res => res.json());
+    return data.searchResult;
+  };
+};
+
+export const useSWRxPlugins = (): SWRResponse<PluginResult | null, Error> => {
+  return useSWR('/extention', pluginsFetcher());
+};
+
+
+// export const useSWRxPlugins = (): SWRResponseWithUtils<Util, IDataAttachmentList, Error> => {
+//   const shouldFetch = pageId != null && pageNumber != null;
+
+//   const fetcher = useCallback(async(endpoint, pageId, pageNumber) => {
+//     const res = await apiv3Get<IResAttachmentList>(endpoint, { pageId, pageNumber });
+//     const resAttachmentList = res.data;
+//     const { paginateResult } = resAttachmentList;
+//     return {
+//       attachments: paginateResult.docs,
+//       totalAttachments: paginateResult.totalDocs,
+//       limit: paginateResult.limit,
+//     };
+//   }, []);
+
+//   const swrResponse = useSWR(
+//     shouldFetch ? ['/attachment/list', pageId, pageNumber] : null,
+//     fetcher,
+//   );
+
+//   // Utils
+//   const remove = useCallback(async(body: { attachment_id: string }) => {
+//     const { mutate } = swrResponse;
+
+//     try {
+//       await apiPost('/attachments.remove', body);
+//       mutate();
+//     }
+//     catch (err) {
+//       throw err;
+//     }
+//   }, [swrResponse]);
+
+//   return withUtils<Util, IDataAttachmentList, Error>(swrResponse, { remove });
+// };