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

Merge branch 'master' into support/gw7735-VRT-side-bar

Mudana-Grune 3 лет назад
Родитель
Сommit
30b0e7cb7f

+ 0 - 2
packages/app/src/client/legacy/crowi.js

@@ -1,5 +1,3 @@
-const { blinkElem, blinkSectionHeaderAtBoot } = require('../util/blink-section-header');
-
 /* eslint-disable react/jsx-filename-extension */
 /* eslint-disable react/jsx-filename-extension */
 require('jquery.cookie');
 require('jquery.cookie');
 
 

+ 1 - 0
packages/app/src/client/util/blink-section-header.ts

@@ -23,5 +23,6 @@ export const blinkSectionHeaderAtBoot = (): HTMLElement | undefined => {
   const elem = document.getElementById(id);
   const elem = document.getElementById(id);
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
   if (elem != null && elem.tagName.match(/h\d+/i)) { // match h1, h2, h3...
     blinkElem(elem);
     blinkElem(elem);
+    return elem;
   }
   }
 };
 };

+ 57 - 18
packages/app/src/components/Page.jsx

@@ -1,16 +1,19 @@
-import React, { useEffect, useRef } from 'react';
+import React, {
+  useCallback, useEffect, useMemo, useRef, useState,
+} from 'react';
 
 
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-
+import { debounce } from 'throttle-debounce';
 
 
 import MarkdownTable from '~/client/models/MarkdownTable';
 import MarkdownTable from '~/client/models/MarkdownTable';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
+import { blinkSectionHeaderAtBoot } from '~/client/util/blink-section-header';
 import { getOptionsToSave } from '~/client/util/editor';
 import { getOptionsToSave } from '~/client/util/editor';
 import GrowiRenderer from '~/services/renderer/growi-renderer';
 import GrowiRenderer from '~/services/renderer/growi-renderer';
 import {
 import {
-  useCurrentPagePath, useIsGuestUser,
+  useCurrentPagePath, useIsGuestUser, useIsBlinkedHeaderAtBoot,
 } from '~/stores/context';
 } from '~/stores/context';
 import { useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors } from '~/stores/editor';
 import { useSWRxSlackChannels, useIsSlackEnabled, usePageTagsForEditors } from '~/stores/editor';
 import { useViewRenderer } from '~/stores/renderer';
 import { useViewRenderer } from '~/stores/renderer';
@@ -196,9 +199,43 @@ const PageWrapper = (props) => {
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupId } = useSelectedGrantGroupId();
   const { data: grantGroupName } = useSelectedGrantGroupName();
   const { data: grantGroupName } = useSelectedGrantGroupName();
   const { data: growiRenderer } = useViewRenderer();
   const { data: growiRenderer } = useViewRenderer();
+  const { data: isBlinkedAtBoot, mutate: mutateBlinkedAtBoot } = useIsBlinkedHeaderAtBoot();
 
 
   const pageRef = useRef(null);
   const pageRef = useRef(null);
 
 
+  // *************************** Blink header at boot ***************************
+  const blinkOnInit = useCallback(() => {
+
+    const result = blinkSectionHeaderAtBoot();
+
+    if (result != null) {
+      mutateBlinkedAtBoot(true);
+    }
+  }, [mutateBlinkedAtBoot]);
+
+  const blinkOnInitDebounced = useMemo(() => debounce(500, blinkOnInit), [blinkOnInit]);
+
+  const observerForBlinkingOnInit = useCallback((elem) => {
+    if (isBlinkedAtBoot || elem == null) {
+      return;
+    }
+
+    const observerCallback = (mutationRecords) => {
+      mutationRecords.forEach(() => blinkOnInitDebounced());
+    };
+
+    const observer = new MutationObserver(observerCallback);
+    observer.observe(elem, { childList: true, subtree: true });
+
+    // first call for the situation that all rendering is complete at this point
+    blinkOnInitDebounced();
+
+    return function cleanup() {
+      observer.disconnect();
+    };
+  }, [blinkOnInitDebounced, isBlinkedAtBoot]);
+  // *******************************  end  *******************************
+
   // set handler to open DrawioModal
   // set handler to open DrawioModal
   useEffect(() => {
   useEffect(() => {
     const handler = (beginLineNumber, endLineNumber) => {
     const handler = (beginLineNumber, endLineNumber) => {
@@ -233,21 +270,23 @@ const PageWrapper = (props) => {
 
 
 
 
   return (
   return (
-    <Page
-      {...props}
-      ref={pageRef}
-      growiRenderer={growiRenderer}
-      pagePath={currentPagePath}
-      editorMode={editorMode}
-      isGuestUser={isGuestUser}
-      isMobile={isMobile}
-      isSlackEnabled={isSlackEnabled}
-      pageTags={pageTags}
-      slackChannels={slackChannelsData.toString()}
-      grant={grant}
-      grantGroupId={grantGroupId}
-      grantGroupName={grantGroupName}
-    />
+    <div ref={observerForBlinkingOnInit}>
+      <Page
+        {...props}
+        ref={pageRef}
+        growiRenderer={growiRenderer}
+        pagePath={currentPagePath}
+        editorMode={editorMode}
+        isGuestUser={isGuestUser}
+        isMobile={isMobile}
+        isSlackEnabled={isSlackEnabled}
+        pageTags={pageTags}
+        slackChannels={slackChannelsData.toString()}
+        grant={grant}
+        grantGroupId={grantGroupId}
+        grantGroupName={grantGroupName}
+      />
+    </div>
   );
   );
 };
 };
 
 

+ 0 - 205
packages/app/src/components/Page/RevisionRenderer.jsx

@@ -1,205 +0,0 @@
-import React from 'react';
-
-import PropTypes from 'prop-types';
-
-import AppContainer from '~/client/services/AppContainer';
-import { blinkElem } from '~/client/util/blink-section-header';
-import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
-import GrowiRenderer from '~/services/renderer/growi-renderer';
-import { useEditorSettings } from '~/stores/editor';
-
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-import RevisionBody from './RevisionBody';
-
-import { loggerFactory } from '^/../codemirror-textlint/src/utils/logger';
-
-const logger = loggerFactory('components:Page:RevisionRenderer');
-
-class LegacyRevisionRenderer extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      html: '',
-    };
-
-    this.renderHtml = this.renderHtml.bind(this);
-    this.getHighlightedBody = this.getHighlightedBody.bind(this);
-  }
-
-  initCurrentRenderingContext() {
-    this.currentRenderingContext = {
-      markdown: this.props.markdown,
-      pagePath: this.props.pagePath,
-      renderDrawioInRealtime: this.props.editorSettings?.renderDrawioInRealtime,
-      currentPathname: decodeURIComponent(window.location.pathname),
-    };
-  }
-
-  componentDidMount() {
-    this.initCurrentRenderingContext();
-    this.renderHtml();
-  }
-
-  componentDidUpdate(prevProps) {
-    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
-    const { markdown, highlightKeywords } = this.props;
-
-    // render only when props.markdown is updated
-    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
-      this.initCurrentRenderingContext();
-      this.renderHtml();
-      return;
-    }
-
-    const HeaderLink = document.getElementsByClassName('revision-head-link');
-    const HeaderLinkArray = Array.from(HeaderLink);
-    addSmoothScrollEvent(HeaderLinkArray, blinkElem);
-
-    const { interceptorManager } = window;
-
-    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
-  }
-
-  /**
-   * transplanted from legacy code -- Yuki Takei
-   * @param {string} body html strings
-   * @param {string} keywords
-   */
-  getHighlightedBody(body, keywords) {
-    const normalizedKeywordsArray = [];
-    // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
-    // Separate keywords
-    // - Surrounded by double quotation
-    // - Split by both full-width and half-width spaces
-    // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
-    keywords.forEach((keyword, i) => {
-      if (keyword === '') {
-        return;
-      }
-      const k = keyword
-        .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
-        .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
-      normalizedKeywordsArray.push(k);
-    });
-
-    const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
-    const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
-    let keywordRegexp2 = keywordRegxp;
-
-    // for non-chrome browsers compatibility
-    try {
-      // eslint-disable-next-line regex/invalid
-      keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
-    }
-    catch (err) {
-      logger.debug('Failed to initialize regex:', err);
-    }
-
-    const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
-    const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
-
-    const insideTagRegex = /<[^<>]*>/g;
-    const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
-
-    const insideTagStrs = body.match(insideTagRegex);
-    const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
-
-    let returnBody = body;
-    const isSafeHtml = insideTagStrs.length === betweenTagMatches.length + 1; // to check whether is safe to join
-    if (isSafeHtml) {
-      // highlight
-      const betweenTagStrs = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
-
-      const arr = [];
-      insideTagStrs.forEach((str, i) => {
-        arr.push(str);
-        arr.push(betweenTagStrs[i]);
-      });
-      returnBody = arr.join('');
-    }
-    else {
-      // inferior highlighter
-      returnBody = highlighter2(body);
-    }
-
-    return returnBody;
-  }
-
-  async renderHtml() {
-    const {
-      appContainer, growiRenderer,
-      highlightKeywords,
-    } = this.props;
-
-    const { interceptorManager } = window;
-    const context = this.currentRenderingContext;
-
-    await interceptorManager.process('preRender', context);
-    await interceptorManager.process('prePreProcess', context);
-    context.markdown = growiRenderer.preProcess(context.markdown, context);
-    await interceptorManager.process('postPreProcess', context);
-    context.parsedHTML = growiRenderer.process(context.markdown, context);
-    await interceptorManager.process('prePostProcess', context);
-    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
-
-    const isMarkdownEmpty = context.markdown.trim().length === 0;
-    if (highlightKeywords != null && highlightKeywords.length > 0 && !isMarkdownEmpty) {
-      context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
-    }
-    await interceptorManager.process('postPostProcess', context);
-    await interceptorManager.process('preRenderHtml', context);
-
-    this.setState({ html: context.parsedHTML });
-  }
-
-  render() {
-    const config = this.props.appContainer.getConfig();
-    const isMathJaxEnabled = !!config.env.MATHJAX;
-
-    return (
-      <RevisionBody
-        html={this.state.html}
-        isMathJaxEnabled={isMathJaxEnabled}
-        additionalClassName={this.props.additionalClassName}
-        renderMathJaxOnInit
-      />
-    );
-  }
-
-}
-
-LegacyRevisionRenderer.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
-  additionalClassName: PropTypes.string,
-  editorSettings: PropTypes.any,
-};
-
-/**
- * Wrapper component for using unstated
- */
-const LegacyRevisionRendererWrapper = withUnstatedContainers(LegacyRevisionRenderer, [AppContainer]);
-
-
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const RevisionRenderer = (props) => {
-  const { data: editorSettings } = useEditorSettings();
-
-  return <LegacyRevisionRendererWrapper {...props} editorSettings={editorSettings} />;
-};
-
-RevisionRenderer.propTypes = {
-  growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
-  markdown: PropTypes.string.isRequired,
-  pagePath: PropTypes.string.isRequired,
-  highlightKeywords: PropTypes.arrayOf(PropTypes.string),
-  additionalClassName: PropTypes.string,
-};
-
-export default RevisionRenderer;

+ 167 - 0
packages/app/src/components/Page/RevisionRenderer.tsx

@@ -0,0 +1,167 @@
+import React, {
+  useCallback, useEffect, useMemo, useState,
+} from 'react';
+
+import { blinkElem } from '~/client/util/blink-section-header';
+import { addSmoothScrollEvent } from '~/client/util/smooth-scroll';
+import { CustomWindow } from '~/interfaces/global';
+import GrowiRenderer from '~/services/renderer/growi-renderer';
+import { useEditorSettings } from '~/stores/editor';
+import loggerFactory from '~/utils/logger';
+
+import RevisionBody from './RevisionBody';
+
+
+const logger = loggerFactory('components:Page:RevisionRenderer');
+
+
+function getHighlightedBody(body: string, _keywords: string | string[]): string {
+  const normalizedKeywordsArray: string[] = [];
+
+  const keywords = (typeof _keywords === 'string') ? [_keywords] : _keywords;
+
+  if (keywords.length === 0) {
+    return body;
+  }
+
+  // !!TODO!!: add test code refs: https://redmine.weseek.co.jp/issues/86841
+  // Separate keywords
+  // - Surrounded by double quotation
+  // - Split by both full-width and half-width spaces
+  // [...keywords.match(/"[^"]+"|[^\u{20}\u{3000}]+/ug)].forEach((keyword, i) => {
+  keywords.forEach((keyword, i) => {
+    if (keyword === '') {
+      return;
+    }
+    const k = keyword
+      .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex operators
+      .replace(/(^"|"$)/g, ''); // for phrase (quoted) keyword
+    normalizedKeywordsArray.push(k);
+  });
+
+  const normalizedKeywords = `(${normalizedKeywordsArray.join('|')})`;
+  const keywordRegxp = new RegExp(`${normalizedKeywords}(?!(.*?"))`, 'ig'); // prior https://regex101.com/r/oX7dq5/1
+  let keywordRegexp2 = keywordRegxp;
+
+  // for non-chrome browsers compatibility
+  try {
+    // eslint-disable-next-line regex/invalid
+    keywordRegexp2 = new RegExp(`(?<!<)${normalizedKeywords}(?!(.*?("|>)))`, 'ig'); // inferior (this doesn't work well when html tags exist a lot) https://regex101.com/r/Dfi61F/1
+  }
+  catch (err) {
+    logger.debug('Failed to initialize regex:', err);
+  }
+
+  const highlighter = (str) => { return str.replace(keywordRegxp, '<em class="highlighted-keyword">$&</em>') }; // prior
+  const highlighter2 = (str) => { return str.replace(keywordRegexp2, '<em class="highlighted-keyword">$&</em>') }; // inferior
+
+  const insideTagRegex = /<[^<>]*>/g;
+  const betweenTagRegex = />([^<>]*)</g; // use (group) to ignore >< around
+
+  const insideTagStrs = body.match(insideTagRegex);
+  const betweenTagMatches = Array.from(body.matchAll(betweenTagRegex));
+
+  let returnBody = body;
+  const isSafeHtml = insideTagStrs?.length === betweenTagMatches.length + 1; // to check whether is safe to join
+  if (isSafeHtml) {
+    // highlight
+    const betweenTagStrs: string[] = betweenTagMatches.map(match => highlighter(match[1])); // get only grouped part (exclude >< around)
+
+    const arr: string[] = [];
+    insideTagStrs.forEach((str, i) => {
+      arr.push(str);
+      arr.push(betweenTagStrs[i]);
+    });
+    returnBody = arr.join('');
+  }
+  else {
+    // inferior highlighter
+    returnBody = highlighter2(body);
+  }
+
+  return returnBody;
+}
+
+
+type Props = {
+  growiRenderer: GrowiRenderer,
+  markdown: string,
+  pagePath: string,
+  highlightKeywords?: string | string[],
+  additionalClassName?: string,
+}
+
+const RevisionRenderer = (props: Props): JSX.Element => {
+
+  const { interceptorManager } = (window as CustomWindow);
+  const {
+    growiRenderer, markdown, pagePath, highlightKeywords,
+  } = props;
+
+  const [html, setHtml] = useState('');
+
+  const { data: editorSettings } = useEditorSettings();
+
+  const currentRenderingContext = useMemo(() => {
+    return {
+      markdown,
+      parsedHTML: '',
+      pagePath,
+      renderDrawioInRealtime: editorSettings?.renderDrawioInRealtime,
+      currentPathname: decodeURIComponent(window.location.pathname),
+    };
+  }, [editorSettings?.renderDrawioInRealtime, markdown, pagePath]);
+
+
+  const renderHtml = useCallback(async() => {
+    if (interceptorManager == null) {
+      return;
+    }
+
+    const context = currentRenderingContext;
+
+    await interceptorManager.process('preRender', context);
+    await interceptorManager.process('prePreProcess', context);
+    context.markdown = growiRenderer.preProcess(context.markdown, context);
+    await interceptorManager.process('postPreProcess', context);
+    context.parsedHTML = growiRenderer.process(context.markdown, context);
+    await interceptorManager.process('prePostProcess', context);
+    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML, context);
+
+    const isMarkdownEmpty = context.markdown.trim().length === 0;
+    if (highlightKeywords != null && !isMarkdownEmpty) {
+      context.parsedHTML = getHighlightedBody(context.parsedHTML, highlightKeywords);
+    }
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderHtml', context);
+
+    setHtml(context.parsedHTML);
+  }, [currentRenderingContext, growiRenderer, highlightKeywords, interceptorManager]);
+
+  useEffect(() => {
+    if (interceptorManager == null) {
+      return;
+    }
+
+    renderHtml()
+      .then(() => {
+        const HeaderLink = document.getElementsByClassName('revision-head-link');
+        const HeaderLinkArray = Array.from(HeaderLink);
+        addSmoothScrollEvent(HeaderLinkArray as HTMLAnchorElement[], blinkElem);
+
+        interceptorManager.process('postRenderHtml', currentRenderingContext);
+      });
+
+  }, [currentRenderingContext, interceptorManager, renderHtml]);
+
+  return (
+    <RevisionBody
+      html={html}
+      additionalClassName={props.additionalClassName}
+      renderMathJaxOnInit
+    />
+  );
+
+};
+
+export default RevisionRenderer;

+ 6 - 0
packages/app/src/interfaces/activity.ts

@@ -114,6 +114,8 @@ const ACTION_ADMIN_CUSTOM_CSS_UPDATE = 'ADMIN_CUSTOM_CSS_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
 const ACTION_ADMIN_ARCHIVE_DATA_CREATE = 'ADMIN_ARCHIVE_DATA_CREATE';
 const ACTION_ADMIN_ARCHIVE_DATA_CREATE = 'ADMIN_ARCHIVE_DATA_CREATE';
+const ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD = 'ADMIN_ARCHIVE_DATA_DOWNLOAD';
+const ACTION_ADMIN_ARCHIVE_DATA_DELETE = 'ADMIN_ARCHIVE_DATA_DELETE';
 const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD = 'ADMIN_USER_NOTIFICATION_SETTINGS_ADD';
 const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD = 'ADMIN_USER_NOTIFICATION_SETTINGS_ADD';
 const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_USER_NOTIFICATION_SETTINGS_DELETE';
 const ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE = 'ADMIN_USER_NOTIFICATION_SETTINGS_DELETE';
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD';
 const ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD = 'ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD';
@@ -280,6 +282,8 @@ export const SupportedAction = {
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
   ACTION_ADMIN_ARCHIVE_DATA_CREATE,
   ACTION_ADMIN_ARCHIVE_DATA_CREATE,
+  ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD,
+  ACTION_ADMIN_ARCHIVE_DATA_DELETE,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD,
@@ -448,6 +452,8 @@ export const LargeActionGroup = {
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
   ACTION_ADMIN_ARCHIVE_DATA_CREATE,
   ACTION_ADMIN_ARCHIVE_DATA_CREATE,
+  ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD,
+  ACTION_ADMIN_ARCHIVE_DATA_DELETE,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_USER_NOTIFICATION_SETTINGS_DELETE,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD,
   ACTION_ADMIN_GLOBAL_NOTIFICATION_SETTINGS_ADD,

+ 10 - 0
packages/app/src/server/routes/admin.js

@@ -356,6 +356,16 @@ module.exports = function(crowi, app) {
 
 
     try {
     try {
       const zipFile = exportService.getFile(fileName);
       const zipFile = exportService.getFile(fileName);
+      const parameters = {
+        ip:  req.ip,
+        endpoint: req.originalUrl,
+        action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_DOWNLOAD,
+        user: req.user?._id,
+        snapshot: {
+          username: req.user?.username,
+        },
+      };
+      crowi.activityService.createActivity(parameters);
       return res.download(zipFile);
       return res.download(zipFile);
     }
     }
     catch (err) {
     catch (err) {

+ 3 - 1
packages/app/src/server/routes/apiv3/export.js

@@ -169,13 +169,15 @@ module.exports = (crowi) => {
    *              schema:
    *              schema:
    *                type: object
    *                type: object
    */
    */
-  router.delete('/:fileName', accessTokenParser, loginRequired, adminRequired, validator.deleteFile, apiV3FormValidator, csrf, async(req, res) => {
+  router.delete('/:fileName', accessTokenParser, loginRequired, adminRequired, validator.deleteFile, apiV3FormValidator, csrf, addActivity, async(req, res) => {
     // TODO: add express validator
     // TODO: add express validator
     const { fileName } = req.params;
     const { fileName } = req.params;
 
 
     try {
     try {
       const zipFile = exportService.getFile(fileName);
       const zipFile = exportService.getFile(fileName);
       fs.unlinkSync(zipFile);
       fs.unlinkSync(zipFile);
+      const parameters = { action: SupportedAction.ACTION_ADMIN_ARCHIVE_DATA_DELETE };
+      activityEvent.emit('update', res.locals.activity._id, parameters);
 
 
       // TODO: use res.apiv3
       // TODO: use res.apiv3
       return res.status(200).send({ ok: true });
       return res.status(200).send({ ok: true });

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

@@ -190,6 +190,10 @@ export const useGrowiRendererConfig = (initialData?: GrowiRendererConfig): SWRRe
   return useStaticSWR('growiRendererConfig', initialData);
   return useStaticSWR('growiRendererConfig', initialData);
 };
 };
 
 
+export const useIsBlinkedHeaderAtBoot = (): SWRResponse<boolean, Error> => {
+  return useStaticSWR('isBlinkedAtBoot', undefined, { fallbackData: false });
+};
+
 
 
 /** **********************************************************
 /** **********************************************************
  *                     Computed contexts
  *                     Computed contexts