فهرست منبع

Merge branch 'master' into support/apply-nextjs-2

Yuki Takei 3 سال پیش
والد
کامیت
333d344a38

+ 2 - 2
package.json

@@ -82,9 +82,9 @@
     "stylelint": "^14.2.0",
     "stylelint-config-recess-order": "^3.0.0",
     "ts-jest": "^27.0.4",
-    "ts-node": "^9.1.1",
+    "ts-node": "^10.9.1",
     "tsconfig-paths": "^3.9.0",
-    "typescript": "^4.7.3",
+    "typescript": "~4.7",
     "yargs": "^17.3.1"
   },
   "engines": {

+ 3 - 0
packages/app/public/static/locales/en_US/admin.json

@@ -529,6 +529,7 @@
     }
   },
   "audit_log_management": {
+    "user": "User",
     "username": "Username",
     "date": "Date",
     "action": "Action",
@@ -536,6 +537,8 @@
     "url": "URL",
     "settings": "Settings",
     "return": "Return",
+    "clear": "Clear search criteria",
+    "reload": "Reload",
     "activity_expiration_date": "Audit Log expiration date",
     "activity_expiration_date_explain": "Created Audit Log are automatically deleted after the number of seconds set in the environment variable from the creation time",
     "fixed_by_env_var": "This is fixed by the env var <code>{{key}}={{value}}</code>.",

+ 1 - 0
packages/app/public/static/locales/en_US/translation.json

@@ -1101,6 +1101,7 @@
       "no_grant_available": "The list of selectable permissions could not be found. Please modify the permissions on the parent page first and try again.",
       "need_to_fix_grant": "The permissions associated with this page must be modified in order to use the functionality correctly. <br> Please select from the options below to make the change.",
       "grant_label": {
+        "public": "Public",
         "isForbidden": "Authority not allowed to view",
         "currentPageGrantLabel": "Authorization for this page: ",
         "parentPageGrantLabel": "Authority of parent page: ",

+ 3 - 0
packages/app/public/static/locales/ja_JP/admin.json

@@ -528,6 +528,7 @@
     }
   },
   "audit_log_management": {
+    "user": "ユーザー",
     "username": "ユーザー名",
     "date": "日付",
     "action": "アクション",
@@ -535,6 +536,8 @@
     "url": "URL",
     "settings": "設定",
     "return": "戻る",
+    "clear": "検索条件のクリア",
+    "reload": "再読み込み",
     "activity_expiration_date": "監査ログの有効期限",
     "activity_expiration_date_explain": "作成された監査ログは、作成時間から環境変数に設定した秒数後に自動的に削除されます",
     "fixed_by_env_var": "環境変数により固定されています <code>{{key}}={{value}}</code>.",

+ 1 - 0
packages/app/public/static/locales/ja_JP/translation.json

@@ -1094,6 +1094,7 @@
       "no_grant_available": "選択可能な権限のリストが見つかりませんでした。まず親ページの権限を修正したのちに再試行してください。",
       "need_to_fix_grant": "正しく機能を使用するためにはこのページに紐づく権限を修正する必要があります。 <br> 下記の選択肢から選んで変更してください。",
       "grant_label": {
+        "public": "公開",
         "isForbidden": "権限の閲覧が許可されていません",
         "currentPageGrantLabel": "このページの権限: ",
         "parentPageGrantLabel": "親のページの権限: ",

+ 3 - 0
packages/app/public/static/locales/zh_CN/admin.json

@@ -538,6 +538,7 @@
     }
   },
   "audit_log_management": {
+    "user": "用户",
     "username": "帐号",
     "date": "日期",
     "action": "行动",
@@ -545,6 +546,8 @@
     "url": "URL",
     "settings": "设置",
     "return": "返回",
+    "clear": "清除搜索标准",
+    "reload": "重新加载",
     "activity_expiration_date": "审计日志的到期日",
     "activity_expiration_date_explain": "创建的审计日志会在环境变量中设置的从创建时间算起的秒数后自动删除",
     "fixed_by_env_var": "这是由env var 修复的 <code>{{key}}={{value}}</code>.",

+ 1 - 0
packages/app/public/static/locales/zh_CN/translation.json

@@ -1104,6 +1104,7 @@
       "no_grant_available": "无法找到可选择的权限列表。 请先修改父页的权限,然后再试一次。",
       "need_to_fix_grant": "为了正确使用该功能,需要修改与该页面相关的权限。 <br> 请从以下选项中选择进行更改。",
       "grant_label": {
+        "public": "向公众提供",
         "isForbidden": "无权查看的机构",
         "currentPageGrantLabel": "本页的权限: ",
         "parentPageGrantLabel": "父页的权限: ",

+ 3 - 0
packages/app/src/client/interfaces/clearable.ts

@@ -0,0 +1,3 @@
+export interface IClearable {
+  clear: () => void,
+}

+ 11 - 2
packages/app/src/components/Admin/AuditLog/ActivityTable.tsx

@@ -1,5 +1,7 @@
 import React, { FC } from 'react';
 
+import { pagePathUtils } from '@growi/core';
+import { UserPicture } from '@growi/ui';
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
@@ -21,7 +23,7 @@ export const ActivityTable : FC<Props> = (props: Props) => {
       <table className="table table-default table-bordered table-user-list">
         <thead>
           <tr>
-            <th scope="col">{t('admin:audit_log_management.username')}</th>
+            <th scope="col">{t('admin:audit_log_management.user')}</th>
             <th scope="col">{t('admin:audit_log_management.date')}</th>
             <th scope="col">{t('admin:audit_log_management.action')}</th>
             <th scope="col">{t('admin:audit_log_management.ip')}</th>
@@ -32,7 +34,14 @@ export const ActivityTable : FC<Props> = (props: Props) => {
           {props.activityList.map((activity) => {
             return (
               <tr data-testid="activity-table" key={activity._id}>
-                <td>{activity.snapshot?.username}</td>
+                <td>
+                  { activity.user != null && (
+                    <>
+                      <UserPicture user={activity.user} className="picture rounded-circle" />
+                      <a className="ml-2" href={pagePathUtils.userPageRoot(activity.user)}>{activity.snapshot?.username}</a>
+                    </>
+                  )}
+                </td>
                 <td>{formatDate(activity.createdAt)}</td>
                 <td>{t(`admin:audit_log_action.${activity.action}`)}</td>
                 <td>{activity.ip}</td>

+ 18 - 3
packages/app/src/components/Admin/AuditLog/SearchUsernameTypeahead.tsx

@@ -1,10 +1,11 @@
 import React, {
-  FC, Fragment, useState, useCallback,
+  Fragment, useState, useCallback, useRef, ForwardRefRenderFunction, forwardRef, useImperativeHandle,
 } from 'react';
 
 import { AsyncTypeahead, Menu, MenuItem } from 'react-bootstrap-typeahead';
 import { useTranslation } from 'react-i18next';
 
+import { IClearable } from '~/client/interfaces/clearable';
 import { useSWRxUsernames } from '~/stores/user';
 
 
@@ -25,10 +26,12 @@ type Props = {
   onChange: (text: string[]) => void
 }
 
-export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
+const SearchUsernameTypeaheadSubstance: ForwardRefRenderFunction<IClearable, Props> = ((props: Props, ref) => {
   const { onChange } = props;
   const { t } = useTranslation();
 
+  const typeaheadRef = useRef<IClearable>(null);
+
   /*
    * State
    */
@@ -96,6 +99,15 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
     );
   }, []);
 
+  useImperativeHandle(ref, () => ({
+    clear() {
+      const instance = typeaheadRef?.current;
+      if (instance != null) {
+        instance.clear();
+      }
+    },
+  }));
+
   return (
     <div className="input-group mr-2">
       <div className="input-group-prepend">
@@ -104,6 +116,7 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
         </span>
       </div>
       <AsyncTypeahead
+        ref={typeaheadRef}
         id="search-username-typeahead-asynctypeahead"
         multiple
         delay={400}
@@ -119,4 +132,6 @@ export const SearchUsernameTypeahead: FC<Props> = (props: Props) => {
       />
     </div>
   );
-};
+});
+
+export const SearchUsernameTypeahead = forwardRef(SearchUsernameTypeaheadSubstance);

+ 32 - 8
packages/app/src/components/Admin/AuditLogManagement.tsx

@@ -1,8 +1,11 @@
-import React, { FC, useState, useCallback } from 'react';
+import React, {
+  FC, useState, useCallback, useRef,
+} from 'react';
 
 import { format } from 'date-fns';
 import { useTranslation } from 'react-i18next';
 
+import { IClearable } from '~/client/interfaces/clearable';
 import { toastError } from '~/client/util/apiNotification';
 import { SupportedActionType } from '~/interfaces/activity';
 import { useSWRxActivity } from '~/stores/activity';
@@ -17,7 +20,6 @@ import { DateRangePicker } from './AuditLog/DateRangePicker';
 import { SearchUsernameTypeahead } from './AuditLog/SearchUsernameTypeahead';
 import { SelectActionDropdown } from './AuditLog/SelectActionDropdown';
 
-
 const formatDate = (date: Date | null) => {
   if (date == null) {
     return '';
@@ -30,8 +32,9 @@ const PAGING_LIMIT = 10;
 export const AuditLogManagement: FC = () => {
   const { t } = useTranslation();
 
+  const typeaheadRef = useRef<IClearable>(null);
+
   const { data: auditLogAvailableActionsData } = useAuditLogAvailableActions();
-  const auditLogAvailableActions = auditLogAvailableActionsData != null ? auditLogAvailableActionsData : [];
 
   /*
    * State
@@ -43,7 +46,7 @@ export const AuditLogManagement: FC = () => {
   const [endDate, setEndDate] = useState<Date | null>(null);
   const [selectedUsernames, setSelectedUsernames] = useState<string[]>([]);
   const [actionMap, setActionMap] = useState(
-    new Map<SupportedActionType, boolean>(auditLogAvailableActions.map(action => [action, true])),
+    new Map<SupportedActionType, boolean>(auditLogAvailableActionsData != null ? auditLogAvailableActionsData.map(action => [action, true]) : []),
   );
 
   /*
@@ -94,6 +97,18 @@ export const AuditLogManagement: FC = () => {
     setSelectedUsernames(usernames);
   }, []);
 
+  const clearButtonPushedHandler = useCallback(() => {
+    setActivePage(1);
+    setStartDate(null);
+    setEndDate(null);
+    setSelectedUsernames([]);
+    typeaheadRef.current?.clear();
+
+    if (auditLogAvailableActionsData != null) {
+      setActionMap(new Map<SupportedActionType, boolean>(auditLogAvailableActionsData.map(action => [action, true])));
+    }
+  }, [setActivePage, setStartDate, setEndDate, setSelectedUsernames, setActionMap, auditLogAvailableActionsData]);
+
   const reloadButtonPushedHandler = useCallback(() => {
     setActivePage(1);
     mutateActivity();
@@ -128,6 +143,7 @@ export const AuditLogManagement: FC = () => {
         <>
           <div className="form-inline mb-3">
             <SearchUsernameTypeahead
+              ref={typeaheadRef}
               onChange={setUsernamesHandler}
             />
 
@@ -139,14 +155,22 @@ export const AuditLogManagement: FC = () => {
 
             <SelectActionDropdown
               actionMap={actionMap}
-              availableActions={auditLogAvailableActions}
+              availableActions={auditLogAvailableActionsData || []}
               onChangeAction={actionCheckboxChangedHandler}
               onChangeMultipleAction={multipleActionCheckboxChangedHandler}
             />
 
-            <button type="button" className="btn ml-auto grw-btn-reload" onClick={reloadButtonPushedHandler}>
-              <i className="icon icon-reload" />
-            </button>
+            <div className="ml-auto">
+              <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={clearButtonPushedHandler}>
+                <span className="icon-refresh mr-1" />
+                {t('admin:audit_log_management.clear')}
+              </button>
+
+              <button type="button" className="btn btn-outline-secondary btn-sm" onClick={reloadButtonPushedHandler}>
+                <i className="icon icon-reload mr-1" />
+                {t('admin:audit_log_management.reload')}
+              </button>
+            </div>
           </div>
 
           <p

+ 1 - 0
packages/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -122,6 +122,7 @@ const AdditionalMenuItems = (props: AdditionalMenuItemsProps): JSX.Element => {
       <DropdownItem
         onClick={() => openAccessoriesModal(PageAccessoriesModalContents.ShareLink)}
         disabled={isGuestUser || isSharedUser || isLinkSharingDisabled}
+        data-testid="open-page-accessories-modal-btn-with-share-link-management-data-tab"
         className="grw-page-control-dropdown-item"
       >
         <span className="grw-page-control-dropdown-icon">

+ 5 - 1
packages/app/src/components/PageAlert/FixPageGrantAlert.tsx

@@ -76,6 +76,10 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       return t('fix_page_grant.modal.grant_label.isForbidden');
     }
 
+    if (grantData.grant === 1) {
+      return t('fix_page_grant.modal.grant_label.public');
+    }
+
     if (grantData.grant === 4) {
       return t('fix_page_grant.modal.radio_btn.only_me');
     }
@@ -87,7 +91,7 @@ const FixPageGrantModal = (props: ModalProps): JSX.Element => {
       return `${t('fix_page_grant.modal.radio_btn.grant_group')}: (${grantData.grantedGroup.name})`;
     }
 
-    throw Error('cannnot get grant label'); // this error can't be throwed
+    throw Error('cannot get grant label'); // this error can't be throwed
   }, [t]);
 
   const renderGrantDataLabel = useCallback(() => {

+ 1 - 1
packages/app/src/components/ShareLink/ShareLink.jsx

@@ -86,7 +86,7 @@ class ShareLink extends React.Component {
     const { t } = this.props;
 
     return (
-      <div className="container p-0">
+      <div className="container p-0" data-testid="share-link-management">
         <h3 className="grw-modal-head d-flex pb-2">
           { t('share_links.share_link_list') }
           <button className="btn btn-danger ml-auto " type="button" onClick={this.deleteAllLinksButtonHandler}>{t('delete_all')}</button>

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

@@ -376,6 +376,8 @@ export const SmallActionGroup = {
   ACTION_USER_LOGOUT,
   ACTION_PAGE_CREATE,
   ACTION_PAGE_DELETE,
+  ACTION_PAGE_DELETE_COMPLETELY,
+  ACTION_PAGE_EMPTY_TRASH,
 } as const;
 
 // SmallActionGroup + Action by all General Users - PAGE_VIEW

+ 0 - 12
packages/app/src/server/models/activity.ts

@@ -127,18 +127,6 @@ activitySchema.statics.updateByParameters = async function(activityId: string, p
   return activity;
 };
 
-activitySchema.statics.getPaginatedActivity = async function(limit: number, offset: number, query) {
-  const paginateResult = await this.paginate(
-    query,
-    {
-      limit,
-      offset,
-      sort: { createdAt: -1 },
-    },
-  );
-  return paginateResult;
-};
-
 activitySchema.statics.findSnapshotUsernamesByUsernameRegexWithTotalCount = async function(
     q: string, option: { sortOpt: number | string, offset: number, limit: number},
 ): Promise<{usernames: string[], totalCount: number}> {

+ 26 - 3
packages/app/src/server/routes/apiv3/activity.ts

@@ -2,12 +2,13 @@ import { parseISO, addMinutes, isValid } from 'date-fns';
 import express, { Request, Router } from 'express';
 import { query } from 'express-validator';
 
-import { ISearchFilter } from '~/interfaces/activity';
+import { IActivity, ISearchFilter } from '~/interfaces/activity';
 import Activity from '~/server/models/activity';
 import loggerFactory from '~/utils/logger';
 
 import Crowi from '../../crowi';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { serializeUserSecurely } from '../../models/serializers/user-serializer';
 
 import { ApiV3Response } from './interfaces/apiv3-response';
 
@@ -92,8 +93,30 @@ module.exports = (crowi: Crowi): Router => {
     }
 
     try {
-      const paginationResult = await Activity.getPaginatedActivity(limit, offset, query);
-      return res.apiv3({ paginationResult });
+      const paginateResult = await Activity.paginate(
+        query,
+        {
+          limit,
+          offset,
+          sort: { createdAt: -1 },
+          populate: 'user',
+        },
+      );
+
+      const User = crowi.model('User');
+      const serializedDocs = paginateResult.docs.map((doc: IActivity) => {
+        if (doc.user != null && doc.user instanceof User) {
+          doc.user = serializeUserSecurely(doc.user);
+        }
+        return doc;
+      });
+
+      const serializedPaginationResult = {
+        ...paginateResult,
+        docs: serializedDocs,
+      };
+
+      return res.apiv3({ serializedPaginationResult });
     }
     catch (err) {
       logger.error('Failed to get paginated activity', err);

+ 1 - 1
packages/app/src/stores/activity.ts

@@ -13,6 +13,6 @@ export const useSWRxActivity = (limit?: number, offset?: number, searchFilter?:
   return useSWRImmutable(
     auditLogEnabled ? ['/activity', limit, offset, stringifiedSearchFilter] : null,
     (endpoint, limit, offset, stringifiedSearchFilter) => apiv3Get(endpoint, { limit, offset, searchFilter: stringifiedSearchFilter })
-      .then(result => result.data.paginationResult),
+      .then(result => result.data.serializedPaginationResult),
   );
 };

+ 11 - 0
packages/app/test/cypress/integration/20-basic-features/use-tools.spec.ts

@@ -139,6 +139,17 @@ context('Page Accessories Modal', () => {
      cy.getByTestid('page-attachment').should('be.visible')
      cy.screenshot(`${ssPrefix}-open-page-attachment-data-bootstrap4`);
   });
+  it('Share Link Management is shown successfully', () => {
+    cy.visit('/Sandbox/Bootstrap4', { });
+    cy.get('#grw-subnav-container').within(() => {
+      cy.getByTestid('open-page-item-control-btn').click();
+      cy.getByTestid('open-page-accessories-modal-btn-with-share-link-management-data-tab').click();
+   });
+
+   cy.getByTestid('page-accessories-modal').should('be.visible');
+   cy.getByTestid('share-link-management').should('be.visible');
+   cy.screenshot(`${ssPrefix}-open-share-link-management-bootstrap4`);
+  });
 
 });
 

+ 1 - 1
packages/slackbot-proxy/package.json

@@ -59,7 +59,7 @@
     "browser-bunyan": "^1.6.3",
     "eslint-plugin-regex": "^1.8.0",
     "morgan": "^1.10.0",
-    "ts-node-dev": "^1.1.6",
+    "ts-node-dev": "^2.0.0",
     "tsc-alias": "^1.2.9"
   }
 }

+ 17 - 34
yarn.lock

@@ -7681,13 +7681,6 @@ dateformat@^3.0.0:
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
   integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
 
-dateformat@~1.0.4-1.2.3:
-  version "1.0.12"
-  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
-  dependencies:
-    get-stdin "^4.0.1"
-    meow "^3.3.0"
-
 dayjs@^1.10.4:
   version "1.10.7"
   resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
@@ -19344,7 +19337,7 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@0.5.19, source-map-support@^0.5.12, source-map-support@^0.5.17:
+source-map-support@0.5.19, source-map-support@^0.5.12:
   version "0.5.19"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
   integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@@ -20825,23 +20818,6 @@ ts-jest@^27.0.4:
     semver "7.x"
     yargs-parser "20.x"
 
-ts-node-dev@^1.1.6:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.6.tgz#ee2113718cb5a92c1c8f4229123ad6afbeba01f8"
-  integrity sha512-RTUi7mHMNQospArGz07KiraQcdgUVNXKsgO2HAi7FoiyPMdTDqdniB6K1dqyaIxT7c9v/VpSbfBZPS6uVpaFLQ==
-  dependencies:
-    chokidar "^3.5.1"
-    dateformat "~1.0.4-1.2.3"
-    dynamic-dedupe "^0.3.0"
-    minimist "^1.2.5"
-    mkdirp "^1.0.4"
-    resolve "^1.0.0"
-    rimraf "^2.6.1"
-    source-map-support "^0.5.12"
-    tree-kill "^1.2.2"
-    ts-node "^9.0.0"
-    tsconfig "^7.0.0"
-
 ts-node-dev@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065"
@@ -20877,16 +20853,23 @@ ts-node@^10.4.0:
     v8-compile-cache-lib "^3.0.1"
     yn "3.1.1"
 
-ts-node@^9.0.0, ts-node@^9.1.1:
-  version "9.1.1"
-  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d"
-  integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg==
+ts-node@^10.9.1:
+  version "10.9.1"
+  resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
+  integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
   dependencies:
+    "@cspotcode/source-map-support" "^0.8.0"
+    "@tsconfig/node10" "^1.0.7"
+    "@tsconfig/node12" "^1.0.7"
+    "@tsconfig/node14" "^1.0.0"
+    "@tsconfig/node16" "^1.0.2"
+    acorn "^8.4.1"
+    acorn-walk "^8.1.1"
     arg "^4.1.0"
     create-require "^1.1.0"
     diff "^4.0.1"
     make-error "^1.1.1"
-    source-map-support "^0.5.17"
+    v8-compile-cache-lib "^3.0.1"
     yn "3.1.1"
 
 tsc-alias@^1.2.9:
@@ -21086,10 +21069,10 @@ typeorm@^0.2.31:
     yargs "^16.2.0"
     zen-observable-ts "^1.0.0"
 
-typescript@^4.7.3:
-  version "4.7.3"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.3.tgz#8364b502d5257b540f9de4c40be84c98e23a129d"
-  integrity sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==
+typescript@~4.7:
+  version "4.7.4"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235"
+  integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==
 
 typpy@2.3.11:
   version "2.3.11"