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

Merge branch 'master' into support/omit-unstated

Yuki Takei 3 лет назад
Родитель
Сommit
e27d094b61

+ 1 - 22
packages/app/src/client/services/PageContainer.js

@@ -17,7 +17,6 @@ import {
 import {
   DrawioInterceptor,
 } from '../util/interceptor/drawio-interceptor';
-import { emojiMartData } from '../util/markdown-it/emoji-mart-data';
 
 const { isTrashPage } = pagePathUtils;
 
@@ -197,30 +196,10 @@ export default class PageContainer extends Container {
 
   async setTocHtml(tocHtml) {
     if (this.state.tocHtml !== tocHtml) {
-      const tocHtmlWithEmoji = await this.colonsToEmoji(tocHtml);
-      this.setState({ tocHtml: tocHtmlWithEmoji });
+      this.setState({ tocHtml });
     }
   }
 
-  /**
-   *
-   * @param {*} html TOC html string
-   * @returns TOC html with emoji (emoji-mart) in URL
-   */
-  async colonsToEmoji(html) {
-    // Emoji colons matching
-    const colons = ':[a-zA-Z0-9-_+]+:';
-    // Emoji with skin tone matching
-    const skin = ':skin-tone-[2-6]:';
-    const colonsRegex = new RegExp(`(${colons}${skin}|${colons})`, 'g');
-    const emojiData = await emojiMartData();
-    return html.replace(colonsRegex, (index, match) => {
-      const emojiName = match.slice(1, -1);
-      return emojiData[emojiName];
-    });
-
-  }
-
   /**
    * save success handler
    * @param {object} page Page instance

+ 16 - 16
packages/app/src/client/util/markdown-it/emoji-mart-data.ts

@@ -3,24 +3,28 @@ import data from 'emoji-mart/data/apple.json';
 
 const DEFAULT_EMOJI_SIZE = 24;
 
+
+type EmojiMap = {
+  [key: string]: string,
+};
+
 /**
  *
  * Get native emoji with skin tone
- * @param emoji Emoji object
  * @param skin number
  * @returns emoji data with skin tone
  */
-const getEmojiSkinTone = async(emoji) => {
+const getEmojiSkinTone = (emojiName: string): EmojiMap => {
   const emojiData = {};
   [...Array(6).keys()].forEach((index) => {
     if (index > 0) {
       const elem = Emoji({
-        emoji,
+        emoji: emojiName,
         skin: index + 1,
         size: DEFAULT_EMOJI_SIZE,
       });
       if (elem) {
-        emojiData[`${emoji}::skin-tone-${index + 1}`] = elem.props['aria-label'].split(',')[0];
+        emojiData[`${emojiName}::skin-tone-${index + 1}`] = elem.props['aria-label'].split(',')[0];
       }
     }
   });
@@ -29,27 +33,29 @@ const getEmojiSkinTone = async(emoji) => {
 
 /**
  * Get native emoji from emoji array
- * @param emojis array of emoji
  * @returns emoji data
  */
 
-const getNativeEmoji = async(emojis) => {
+const getNativeEmoji = (): EmojiMap => {
   const emojiData = {};
-  emojis.forEach(async(emoji) => {
+  Object.entries(data.emojis).forEach((emoji) => {
     const emojiName = emoji[0];
-    const hasSkinVariation = emoji[1].skin_variations;
+    const hasSkinVariation = 'skin_variations' in emoji[1];
+
     const elem = Emoji({
       emoji: emojiName,
       size: DEFAULT_EMOJI_SIZE,
     });
+
     if (elem != null) {
       emojiData[emojiName] = elem.props['aria-label'].split(',')[0];
       if (hasSkinVariation) {
-        const emojiWithSkinTone = await getEmojiSkinTone(emojiName);
+        const emojiWithSkinTone = getEmojiSkinTone(emojiName);
         Object.assign(emojiData, emojiWithSkinTone);
       }
     }
   });
+
   return emojiData;
 };
 
@@ -57,10 +63,4 @@ const getNativeEmoji = async(emojis) => {
  * Get native emoji mart data
  * @returns native emoji mart data
  */
-
-export const emojiMartData = () => {
-  const emojis = Object.entries(data.emojis).map((emoji) => {
-    return emoji;
-  });
-  return getNativeEmoji(emojis);
-};
+export const emojiMartData = getNativeEmoji();

+ 4 - 7
packages/app/src/client/util/markdown-it/emoji.js

@@ -1,15 +1,12 @@
+import markdownItEmojiMart from 'markdown-it-emoji-mart';
+
 import { emojiMartData } from './emoji-mart-data';
 
-export default class EmojiConfigurer {
 
-  constructor(crowi) {
-    this.crowi = crowi;
-  }
+export default class EmojiConfigurer {
 
   configure(md) {
-    emojiMartData().then((data) => {
-      md.use(require('markdown-it-emoji-mart'), { defs: data });
-    });
+    md.use(markdownItEmojiMart, { defs: emojiMartData });
   }
 
 }

+ 13 - 7
packages/app/src/client/util/markdown-it/toc-and-anchor.js

@@ -1,3 +1,8 @@
+import markdownItEmojiMart from 'markdown-it-emoji-mart';
+import markdownItToc from 'markdown-it-toc-and-anchor-with-slugid';
+
+import { emojiMartData } from './emoji-mart-data';
+
 export default class TocAndAnchorConfigurer {
 
   constructor(crowi, setHtml) {
@@ -6,13 +11,14 @@ export default class TocAndAnchorConfigurer {
   }
 
   configure(md) {
-    md.use(require('markdown-it-toc-and-anchor-with-slugid').default, {
-      tocLastLevel: 3,
-      anchorLinkBefore: false,
-      anchorLinkSymbol: '',
-      anchorLinkSymbolClassName: 'icon-link',
-      anchorClassName: 'revision-head-link',
-    });
+    md.use(markdownItEmojiMart, { defs: emojiMartData })
+      .use(markdownItToc, {
+        tocLastLevel: 3,
+        anchorLinkBefore: false,
+        anchorLinkSymbol: '',
+        anchorLinkSymbolClassName: 'icon-link',
+        anchorClassName: 'revision-head-link',
+      });
 
     // set toc render function
     if (this.setHtml != null) {

+ 1 - 1
packages/app/src/server/service/page.ts

@@ -3418,7 +3418,7 @@ class PageService {
     }
 
     // Prepare a page document
-    const shouldNew = !isGrantRestricted;
+    const shouldNew = isGrantRestricted;
     const page = await this.preparePageDocumentToCreate(path, shouldNew);
 
     // Set field

+ 223 - 35
packages/app/test/integration/service/v5.migration.test.js

@@ -34,6 +34,16 @@ describe('V5 page migration', () => {
   const pageId10 = new mongoose.Types.ObjectId();
   const pageId11 = new mongoose.Types.ObjectId();
 
+  const public = filter => ({ grant: Page.GRANT_PUBLIC, ...filter });
+  const ownedByTestUser1 = filter => ({ grant: Page.GRANT_OWNER, grantedUsers: [testUser1._id], ...filter });
+  const root = filter => ({ grantedUsers: [rootUser._id], ...filter });
+  const rootUserGroup = filter => ({ grantedGroup: rootUserGroupId, ...filter });
+  const testUser1Group = filter => ({ grantedGroup: testUser1GroupId, ...filter });
+
+  const normalized = { parent: { $ne: null } };
+  const notNormalized = { parent: null };
+  const empty = { isEmpty: true };
+
   beforeAll(async() => {
     jest.restoreAllMocks();
 
@@ -392,14 +402,6 @@ describe('V5 page migration', () => {
      *     - /normalize_g/normalize_i/normalize_k (only me) is normalized
      */
 
-    const public = filter => ({ grant: Page.GRANT_PUBLIC, ...filter });
-    const owned = filter => ({ grant: Page.GRANT_OWNER, grantedUsers: [testUser1._id], ...filter });
-    const testUser1Group = filter => ({ grantedGroup: testUser1GroupId, ...filter });
-
-    const normalized = { parent: { $ne: null } };
-    const notNormalized = { parent: null };
-    const empty = { isEmpty: true };
-
     beforeAll(async() => {
       // Prepare data
       const id1 = new mongoose.Types.ObjectId();
@@ -555,10 +557,10 @@ describe('V5 page migration', () => {
 
     test('should replace all unnecessary empty pages and normalization succeeds', async() => {
       const _pageG = await Page.findOne(public({ path: '/normalize_g', ...normalized }));
-      const _pageGH = await Page.findOne(owned({ path: '/normalize_g/normalize_h', ...notNormalized }));
-      const _pageGI = await Page.findOne(owned({ path: '/normalize_g/normalize_i', ...notNormalized }));
-      const _pageGHJ = await Page.findOne(owned({ path: '/normalize_g/normalize_h/normalize_j', ...notNormalized }));
-      const _pageGIK = await Page.findOne(owned({ path: '/normalize_g/normalize_i/normalize_k', ...notNormalized }));
+      const _pageGH = await Page.findOne(ownedByTestUser1({ path: '/normalize_g/normalize_h', ...notNormalized }));
+      const _pageGI = await Page.findOne(ownedByTestUser1({ path: '/normalize_g/normalize_i', ...notNormalized }));
+      const _pageGHJ = await Page.findOne(ownedByTestUser1({ path: '/normalize_g/normalize_h/normalize_j', ...notNormalized }));
+      const _pageGIK = await Page.findOne(ownedByTestUser1({ path: '/normalize_g/normalize_i/normalize_k', ...notNormalized }));
 
       expect(_pageG).not.toBeNull();
       expect(_pageGH).not.toBeNull();
@@ -608,8 +610,8 @@ describe('V5 page migration', () => {
       expect(pageGIK.descendantCount).toStrictEqual(0);
 
       // -- not normalized pages
-      const pageGH = await Page.findOne(owned({ path: '/normalize_g/normalize_h' }));
-      const pageGI = await Page.findOne(owned({ path: '/normalize_g/normalize_i' }));
+      const pageGH = await Page.findOne(ownedByTestUser1({ path: '/normalize_g/normalize_h' }));
+      const pageGI = await Page.findOne(ownedByTestUser1({ path: '/normalize_g/normalize_i' }));
       // Check existence
       expect(pageGH).not.toBeNull();
       expect(pageGI).not.toBeNull();
@@ -655,15 +657,6 @@ describe('V5 page migration', () => {
      *     - E and F are NOT normalized
      */
 
-    const owned = filter => ({ grantedUsers: [testUser1._id], ...filter });
-    const root = filter => ({ grantedUsers: [rootUser._id], ...filter });
-    const rootUserGroup = filter => ({ grantedGroup: rootUserGroupId, ...filter });
-    const testUser1Group = filter => ({ grantedGroup: testUser1GroupId, ...filter });
-
-    const normalized = { parent: { $ne: null } };
-    const notNormalized = { parent: null };
-    const empty = { isEmpty: true };
-
     beforeAll(async() => {
       // Prepare data
       const id17 = new mongoose.Types.ObjectId();
@@ -787,10 +780,10 @@ describe('V5 page migration', () => {
 
 
     test('Should normalize a single page without including other pages', async() => {
-      const _owned13 = await Page.findOne(owned({ path: '/normalize_13_owned', ...notNormalized }));
-      const _owned14 = await Page.findOne(owned({ path: '/normalize_13_owned/normalize_14_owned', ...notNormalized }));
-      const _owned15 = await Page.findOne(owned({ path: '/normalize_13_owned/normalize_14_owned/normalize_15_owned', ...notNormalized }));
-      const _owned16 = await Page.findOne(owned({ path: '/normalize_13_owned/normalize_14_owned/normalize_15_owned/normalize_16_owned', ...notNormalized }));
+      const _owned13 = await Page.findOne(ownedByTestUser1({ path: '/normalize_13_owned', ...notNormalized }));
+      const _owned14 = await Page.findOne(ownedByTestUser1({ path: '/normalize_13_owned/normalize_14_owned', ...notNormalized }));
+      const _owned15 = await Page.findOne(ownedByTestUser1({ path: '/normalize_13_owned/normalize_14_owned/normalize_15_owned', ...notNormalized }));
+      const _owned16 = await Page.findOne(ownedByTestUser1({ path: '/normalize_13_owned/normalize_14_owned/normalize_15_owned/normalize_16_owned', ...notNormalized }));
       const _root16 = await Page.findOne(root({ path: '/normalize_13_owned/normalize_14_owned/normalize_15_owned/normalize_16_root', ...notNormalized }));
       const _group16 = await Page.findOne(testUser1Group({ path: '/normalize_13_owned/normalize_14_owned/normalize_15_owned/normalize_16_group', ...notNormalized }));
 
@@ -836,10 +829,10 @@ describe('V5 page migration', () => {
     });
 
     test('Should normalize pages recursively excluding the pages not selected', async() => {
-      const _owned17 = await Page.findOne(owned({ path: '/normalize_17_owned', ...normalized }));
-      const _owned18 = await Page.findOne(owned({ path: '/normalize_17_owned/normalize_18_owned', ...normalized }));
-      const _owned19 = await Page.findOne(owned({ path: '/normalize_17_owned/normalize_18_owned/normalize_19_owned', ...notNormalized }));
-      const _owned20 = await Page.findOne(owned({ path: '/normalize_17_owned/normalize_18_owned/normalize_19_owned/normalize_20_owned', ...notNormalized }));
+      const _owned17 = await Page.findOne(ownedByTestUser1({ path: '/normalize_17_owned', ...normalized }));
+      const _owned18 = await Page.findOne(ownedByTestUser1({ path: '/normalize_17_owned/normalize_18_owned', ...normalized }));
+      const _owned19 = await Page.findOne(ownedByTestUser1({ path: '/normalize_17_owned/normalize_18_owned/normalize_19_owned', ...notNormalized }));
+      const _owned20 = await Page.findOne(ownedByTestUser1({ path: '/normalize_17_owned/normalize_18_owned/normalize_19_owned/normalize_20_owned', ...notNormalized }));
       const _root20 = await Page.findOne(root({ path: '/normalize_17_owned/normalize_18_owned/normalize_19_owned/normalize_20_root', ...notNormalized }));
       const _group20 = await Page.findOne(rootUserGroup({ path: '/normalize_17_owned/normalize_18_owned/normalize_19_owned/normalize_20_group', ...notNormalized }));
 
@@ -884,11 +877,11 @@ describe('V5 page migration', () => {
     });
 
     test('Should normalize pages recursively excluding the pages of not user\'s & Should delete unnecessary empty pages', async() => {
-      const _owned21 = await Page.findOne(owned({ path: '/normalize_21_owned', ...normalized }));
-      const _owned22 = await Page.findOne(owned({ path: '/normalize_21_owned/normalize_22_owned', ...normalized }));
-      const _owned23 = await Page.findOne(owned({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned', ...notNormalized }));
+      const _owned21 = await Page.findOne(ownedByTestUser1({ path: '/normalize_21_owned', ...normalized }));
+      const _owned22 = await Page.findOne(ownedByTestUser1({ path: '/normalize_21_owned/normalize_22_owned', ...normalized }));
+      const _owned23 = await Page.findOne(ownedByTestUser1({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned', ...notNormalized }));
       const _empty23 = await Page.findOne({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned', ...normalized, ...empty });
-      const _owned24 = await Page.findOne(owned({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned/normalize_24_owned', ...normalized }));
+      const _owned24 = await Page.findOne(ownedByTestUser1({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned/normalize_24_owned', ...normalized }));
       const _root24 = await Page.findOne(root({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned/normalize_24_root', ...notNormalized }));
       const _rootGroup24 = await Page.findOne(rootUserGroup({ path: '/normalize_21_owned/normalize_22_owned/normalize_23_owned/normalize_24_rootGroup', ...notNormalized }));
 
@@ -1116,4 +1109,199 @@ describe('V5 page migration', () => {
     expect(privatePage).toStrictEqual(expectedPrivatePage);
   });
 
+  describe('normalizeParentByPath', () => {
+    const normalizeParentByPath = async(path, user) => {
+      const mock = jest.spyOn(crowi.pageService, 'normalizeParentRecursivelyMainOperation').mockReturnValue(null);
+      const result = await crowi.pageService.normalizeParentByPath(path, user);
+      const args = mock.mock.calls[0];
+
+      mock.mockRestore();
+
+      await crowi.pageService.normalizeParentRecursivelyMainOperation(...args);
+
+      return result;
+    };
+
+    beforeAll(async() => {
+      const pageIdD = new mongoose.Types.ObjectId();
+      const pageIdG = new mongoose.Types.ObjectId();
+
+      await Page.insertMany([
+        {
+          path: '/norm_parent_by_path_A',
+          grant: Page.GRANT_OWNER,
+          grantedUsers: [testUser1._id],
+          creator: testUser1._id,
+          lastUpdateUser: testUser1._id,
+          parent: rootPage._id,
+        },
+        {
+          path: '/norm_parent_by_path_B/norm_parent_by_path_C',
+          grant: Page.GRANT_OWNER,
+          grantedUsers: [rootUser._id],
+          creator: rootUser._id,
+          lastUpdateUser: rootUser._id,
+        },
+        {
+          _id: pageIdD,
+          path: '/norm_parent_by_path_D',
+          isEmpty: true,
+          parent: rootPage._id,
+          descendantCount: 1,
+        },
+        {
+          path: '/norm_parent_by_path_D/norm_parent_by_path_E',
+          grant: Page.GRANT_PUBLIC,
+          creator: rootUser._id,
+          lastUpdateUser: rootUser._id,
+          parent: pageIdD,
+        },
+        {
+          path: '/norm_parent_by_path_D/norm_parent_by_path_F',
+          grant: Page.GRANT_OWNER,
+          grantedUsers: [rootUser._id],
+          creator: rootUser._id,
+          lastUpdateUser: rootUser._id,
+        },
+        {
+          _id: pageIdG,
+          path: '/norm_parent_by_path_G',
+          grant: Page.GRANT_PUBLIC,
+          creator: rootUser._id,
+          lastUpdateUser: rootUser._id,
+          parent: rootPage._id,
+          descendantCount: 1,
+        },
+        {
+          path: '/norm_parent_by_path_G/norm_parent_by_path_H',
+          grant: Page.GRANT_PUBLIC,
+          creator: rootUser._id,
+          lastUpdateUser: rootUser._id,
+          parent: pageIdG,
+        },
+        {
+          path: '/norm_parent_by_path_G/norm_parent_by_path_I',
+          grant: Page.GRANT_OWNER,
+          grantedUsers: [rootUser._id],
+          creator: rootUser._id,
+          lastUpdateUser: rootUser._id,
+        },
+      ]);
+    });
+
+    test('should fail when the user is not allowed to edit the target page found by path', async() => {
+      const pageTestUser1 = await Page.findOne(ownedByTestUser1({ path: '/norm_parent_by_path_A' }));
+
+      expect(pageTestUser1).not.toBeNull();
+
+      await expect(normalizeParentByPath('/norm_parent_by_path_A', rootUser)).rejects.toThrowError();
+    });
+
+    test('should normalize all granted pages under the path when no page exists at the path', async() => {
+      const _pageB = await Page.findOne({ path: '/norm_parent_by_path_B' });
+      const _pageBC = await Page.findOne(root({ path: '/norm_parent_by_path_B/norm_parent_by_path_C' }));
+
+      expect(_pageB).toBeNull();
+      expect(_pageBC).not.toBeNull();
+
+      await normalizeParentByPath('/norm_parent_by_path_B', rootUser);
+
+      const pagesB = await Page.find({ path: '/norm_parent_by_path_B' }); // did not exist before running normalizeParentByPath
+      const pageBC = await Page.findById(_pageBC._id);
+
+      // -- check count
+      expect(pagesB.length).toBe(1);
+
+      const pageB = pagesB[0];
+
+      // -- check existance
+      expect(pageB.path).toBe('/norm_parent_by_path_B');
+      expect(pageBC.path).toBe('/norm_parent_by_path_B/norm_parent_by_path_C');
+
+      // -- check parent
+      expect(pageB.parent).toStrictEqual(rootPage._id);
+      expect(pageBC.parent).toStrictEqual(pageB._id);
+
+      // -- check descendantCount
+      expect(pageB.descendantCount).toBe(1);
+      expect(pageBC.descendantCount).toBe(0);
+    });
+
+    test('should normalize all granted pages under the path when an empty page exists at the path', async() => {
+      const _emptyD = await Page.findOne({ path: '/norm_parent_by_path_D', ...empty, ...normalized });
+      const _pageDE = await Page.findOne(public({ path: '/norm_parent_by_path_D/norm_parent_by_path_E', ...normalized }));
+      const _pageDF = await Page.findOne(root({ path: '/norm_parent_by_path_D/norm_parent_by_path_F', ...notNormalized }));
+
+      expect(_emptyD).not.toBeNull();
+      expect(_pageDE).not.toBeNull();
+      expect(_pageDF).not.toBeNull();
+
+      await normalizeParentByPath('/norm_parent_by_path_D', rootUser);
+
+      const countD = await Page.count({ path: '/norm_parent_by_path_D' });
+
+      // -- check count
+      expect(countD).toBe(1);
+
+      const pageD = await Page.findById(_emptyD._id);
+      const pageDE = await Page.findById(_pageDE._id);
+      const pageDF = await Page.findById(_pageDF._id);
+
+      // -- check existance
+      expect(pageD.path).toBe('/norm_parent_by_path_D');
+      expect(pageDE.path).toBe('/norm_parent_by_path_D/norm_parent_by_path_E');
+      expect(pageDF.path).toBe('/norm_parent_by_path_D/norm_parent_by_path_F');
+
+      // -- check isEmpty of pageD
+      // pageD should not be empty because growi system will create a non-empty page while running normalizeParentByPath
+      expect(pageD.isEmpty).toBe(false);
+
+      // -- check parent
+      expect(pageD.parent).toStrictEqual(rootPage._id);
+      expect(pageDE.parent).toStrictEqual(pageD._id);
+      expect(pageDF.parent).toStrictEqual(pageD._id);
+
+      // -- check descendantCount
+      expect(pageD.descendantCount).toBe(2);
+      expect(pageDE.descendantCount).toBe(0);
+      expect(pageDF.descendantCount).toBe(0);
+    });
+
+    test('should normalize all granted pages under the path when a non-empty page exists at the path', async() => {
+      const _pageG = await Page.findOne(public({ path: '/norm_parent_by_path_G', ...normalized }));
+      const _pageGH = await Page.findOne(public({ path: '/norm_parent_by_path_G/norm_parent_by_path_H', ...normalized }));
+      const _pageGI = await Page.findOne(root({ path: '/norm_parent_by_path_G/norm_parent_by_path_I', ...notNormalized }));
+
+      expect(_pageG).not.toBeNull();
+      expect(_pageGH).not.toBeNull();
+      expect(_pageGI).not.toBeNull();
+
+      await normalizeParentByPath('/norm_parent_by_path_G', rootUser);
+
+      const countG = await Page.count({ path: '/norm_parent_by_path_G' });
+
+      // -- check count
+      expect(countG).toBe(1);
+
+      const pageG = await Page.findById(_pageG._id);
+      const pageGH = await Page.findById(_pageGH._id);
+      const pageGI = await Page.findById(_pageGI._id);
+
+      // -- check existance
+      expect(pageG.path).toBe('/norm_parent_by_path_G');
+      expect(pageGH.path).toBe('/norm_parent_by_path_G/norm_parent_by_path_H');
+      expect(pageGI.path).toBe('/norm_parent_by_path_G/norm_parent_by_path_I');
+
+      // -- check parent
+      expect(pageG.parent).toStrictEqual(rootPage._id);
+      expect(pageGH.parent).toStrictEqual(pageG._id);
+      expect(pageGI.parent).toStrictEqual(pageG._id);
+
+      // -- check descendantCount
+      expect(pageG.descendantCount).toBe(2);
+      expect(pageGH.descendantCount).toBe(0);
+      expect(pageGI.descendantCount).toBe(0);
+    });
+  });
+
 });