feat: add page titles using React Helmet (#1321)
* feat: add page titles using React Helmet * refactor: extract page title logic to separate component * fix: resolve review comments * fix: resolve testing errors
This commit is contained in:
@ -19,6 +19,7 @@
|
||||
"@types/node": "^16.18.4",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/react-helmet-async": "^1.0.3",
|
||||
"@types/react-modal": "^3.16.0",
|
||||
"afterframe": "^1.0.2",
|
||||
"apollo-link-rest": "^0.9.0",
|
||||
@ -41,6 +42,7 @@
|
||||
"react-datepicker": "^4.11.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-hook-form": "^7.45.1",
|
||||
"react-hotkeys-hook": "^4.4.0",
|
||||
"react-loading-skeleton": "^3.3.1",
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { DefaultLayout } from '@/ui/layout/components/DefaultLayout';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { CreateProfile } from '~/pages/auth/CreateProfile';
|
||||
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
|
||||
import { SignInUp } from '~/pages/auth/SignInUp';
|
||||
@ -21,13 +22,18 @@ import { Tasks } from '~/pages/tasks/Tasks';
|
||||
import { AppInternalHooks } from '~/sync-hooks/AppInternalHooks';
|
||||
|
||||
import { NotFound } from './pages/not-found/NotFound';
|
||||
import { getPageTitleFromPath } from './utils/title-utils';
|
||||
|
||||
// TEMP FEATURE FLAG FOR VIEW FIELDS
|
||||
export const ACTIVATE_VIEW_FIELDS = true;
|
||||
|
||||
export function App() {
|
||||
const { pathname } = useLocation();
|
||||
const pageTitle = getPageTitleFromPath(pathname);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title={pageTitle} />
|
||||
<AppInternalHooks />
|
||||
<DefaultLayout>
|
||||
<Routes>
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@ -24,7 +25,9 @@ const meta: Meta<typeof App> = {
|
||||
<MemoryRouter>
|
||||
<FullHeightStorybookLayout>
|
||||
<MockedAuth>
|
||||
<Story />
|
||||
<HelmetProvider>
|
||||
<Story />
|
||||
</HelmetProvider>
|
||||
</MockedAuth>
|
||||
</FullHeightStorybookLayout>
|
||||
</MemoryRouter>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { StrictMode } from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
@ -27,20 +28,22 @@ root.render(
|
||||
<RecoilRoot>
|
||||
<BrowserRouter>
|
||||
<ApolloProvider>
|
||||
<ClientConfigProvider>
|
||||
<UserProvider>
|
||||
<PageChangeEffect />
|
||||
<AppThemeProvider>
|
||||
<SnackBarProvider>
|
||||
<DialogProvider>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</DialogProvider>
|
||||
</SnackBarProvider>
|
||||
</AppThemeProvider>
|
||||
</UserProvider>
|
||||
</ClientConfigProvider>
|
||||
<HelmetProvider>
|
||||
<ClientConfigProvider>
|
||||
<UserProvider>
|
||||
<PageChangeEffect />
|
||||
<AppThemeProvider>
|
||||
<SnackBarProvider>
|
||||
<DialogProvider>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</DialogProvider>
|
||||
</SnackBarProvider>
|
||||
</AppThemeProvider>
|
||||
</UserProvider>
|
||||
</ClientConfigProvider>
|
||||
</HelmetProvider>
|
||||
</ApolloProvider>
|
||||
</BrowserRouter>
|
||||
</RecoilRoot>,
|
||||
|
||||
13
front/src/modules/ui/utilities/page-title/PageTitle.tsx
Normal file
13
front/src/modules/ui/utilities/page-title/PageTitle.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
|
||||
type OwnProps = {
|
||||
title: string;
|
||||
};
|
||||
|
||||
export function PageTitle({ title }: OwnProps) {
|
||||
return (
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
@ -17,6 +17,7 @@ import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPage
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { useUpdateOneCompanyMutation } from '~/generated/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
@ -45,66 +46,71 @@ export function CompanyShow() {
|
||||
}
|
||||
|
||||
return (
|
||||
<WithTopBarContainer
|
||||
title={company.name ?? ''}
|
||||
hasBackButton
|
||||
isFavorite={isFavorite}
|
||||
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
||||
onFavoriteButtonClick={handleFavoriteButtonClick}
|
||||
extraButtons={[
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: company.id,
|
||||
type: ActivityTargetableEntityType.Company,
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<RecoilScope SpecificContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
<ShowPageSummaryCard
|
||||
id={company.id}
|
||||
logoOrAvatar={getLogoUrlFromDomainName(company.domainName ?? '')}
|
||||
title={company.name ?? 'No name'}
|
||||
date={company.createdAt ?? ''}
|
||||
renderTitleEditComponent={() => (
|
||||
<CompanyNameEditableField company={company} />
|
||||
)}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
<EditableFieldMutationContext.Provider
|
||||
value={useUpdateOneCompanyMutation}
|
||||
>
|
||||
<EditableFieldEntityIdContext.Provider value={company.id}>
|
||||
{companyShowFieldDefinition.map((fieldDefinition) => {
|
||||
return (
|
||||
<EditableFieldDefinitionContext.Provider
|
||||
value={fieldDefinition}
|
||||
key={fieldDefinition.id}
|
||||
>
|
||||
<GenericEditableField />
|
||||
</EditableFieldDefinitionContext.Provider>
|
||||
);
|
||||
})}
|
||||
</EditableFieldEntityIdContext.Provider>
|
||||
</EditableFieldMutationContext.Provider>
|
||||
</PropertyBox>
|
||||
<CompanyTeam company={company}></CompanyTeam>
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
<>
|
||||
<PageTitle title={company.name || 'No Name'} />
|
||||
<WithTopBarContainer
|
||||
title={company.name ?? ''}
|
||||
hasBackButton
|
||||
isFavorite={isFavorite}
|
||||
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
||||
onFavoriteButtonClick={handleFavoriteButtonClick}
|
||||
extraButtons={[
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: company.id,
|
||||
type: ActivityTargetableEntityType.Company,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<RecoilScope SpecificContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
<ShowPageSummaryCard
|
||||
id={company.id}
|
||||
logoOrAvatar={getLogoUrlFromDomainName(
|
||||
company.domainName ?? '',
|
||||
)}
|
||||
title={company.name ?? 'No name'}
|
||||
date={company.createdAt ?? ''}
|
||||
renderTitleEditComponent={() => (
|
||||
<CompanyNameEditableField company={company} />
|
||||
)}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
<EditableFieldMutationContext.Provider
|
||||
value={useUpdateOneCompanyMutation}
|
||||
>
|
||||
<EditableFieldEntityIdContext.Provider value={company.id}>
|
||||
{companyShowFieldDefinition.map((fieldDefinition) => {
|
||||
return (
|
||||
<EditableFieldDefinitionContext.Provider
|
||||
value={fieldDefinition}
|
||||
key={fieldDefinition.id}
|
||||
>
|
||||
<GenericEditableField />
|
||||
</EditableFieldDefinitionContext.Provider>
|
||||
);
|
||||
})}
|
||||
</EditableFieldEntityIdContext.Provider>
|
||||
</EditableFieldMutationContext.Provider>
|
||||
</PropertyBox>
|
||||
<CompanyTeam company={company}></CompanyTeam>
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
entity={{
|
||||
id: company.id,
|
||||
type: ActivityTargetableEntityType.Company,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPage
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/PageTitle';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import {
|
||||
useUpdateOnePersonMutation,
|
||||
@ -63,66 +64,73 @@ export function PersonShow() {
|
||||
}
|
||||
|
||||
return (
|
||||
<WithTopBarContainer
|
||||
title={person.firstName ?? ''}
|
||||
icon={<IconUser size={theme.icon.size.md} />}
|
||||
hasBackButton
|
||||
isFavorite={isFavorite}
|
||||
onFavoriteButtonClick={handleFavoriteButtonClick}
|
||||
extraButtons={[
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: person.id,
|
||||
type: ActivityTargetableEntityType.Person,
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<RecoilScope SpecificContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
<ShowPageSummaryCard
|
||||
id={person.id}
|
||||
title={person.displayName ?? 'No name'}
|
||||
logoOrAvatar={person.avatarUrl ?? undefined}
|
||||
date={person.createdAt ?? ''}
|
||||
renderTitleEditComponent={() =>
|
||||
person ? <PeopleFullNameEditableField people={person} /> : <></>
|
||||
}
|
||||
onUploadPicture={onUploadPicture}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
<EditableFieldMutationContext.Provider
|
||||
value={useUpdateOnePersonMutation}
|
||||
>
|
||||
<EditableFieldEntityIdContext.Provider value={person.id}>
|
||||
{personShowFieldDefinition.map((fieldDefinition) => {
|
||||
return (
|
||||
<EditableFieldDefinitionContext.Provider
|
||||
value={fieldDefinition}
|
||||
key={fieldDefinition.id}
|
||||
>
|
||||
<GenericEditableField />
|
||||
</EditableFieldDefinitionContext.Provider>
|
||||
);
|
||||
})}
|
||||
</EditableFieldEntityIdContext.Provider>
|
||||
</EditableFieldMutationContext.Provider>
|
||||
</PropertyBox>
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
<>
|
||||
<PageTitle title={person.displayName || 'No Name'} />
|
||||
<WithTopBarContainer
|
||||
title={person.firstName ?? ''}
|
||||
icon={<IconUser size={theme.icon.size.md} />}
|
||||
hasBackButton
|
||||
isFavorite={isFavorite}
|
||||
onFavoriteButtonClick={handleFavoriteButtonClick}
|
||||
extraButtons={[
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: person.id ?? '',
|
||||
id: person.id,
|
||||
type: ActivityTargetableEntityType.Person,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<RecoilScope SpecificContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
<ShowPageSummaryCard
|
||||
id={person.id}
|
||||
title={person.displayName ?? 'No name'}
|
||||
logoOrAvatar={person.avatarUrl ?? undefined}
|
||||
date={person.createdAt ?? ''}
|
||||
renderTitleEditComponent={() =>
|
||||
person ? (
|
||||
<PeopleFullNameEditableField people={person} />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
onUploadPicture={onUploadPicture}
|
||||
/>
|
||||
<PropertyBox extraPadding={true}>
|
||||
<EditableFieldMutationContext.Provider
|
||||
value={useUpdateOnePersonMutation}
|
||||
>
|
||||
<EditableFieldEntityIdContext.Provider value={person.id}>
|
||||
{personShowFieldDefinition.map((fieldDefinition) => {
|
||||
return (
|
||||
<EditableFieldDefinitionContext.Provider
|
||||
value={fieldDefinition}
|
||||
key={fieldDefinition.id}
|
||||
>
|
||||
<GenericEditableField />
|
||||
</EditableFieldDefinitionContext.Provider>
|
||||
);
|
||||
})}
|
||||
</EditableFieldEntityIdContext.Provider>
|
||||
</EditableFieldMutationContext.Provider>
|
||||
</PropertyBox>
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer
|
||||
entity={{
|
||||
id: person.id ?? '',
|
||||
type: ActivityTargetableEntityType.Person,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { HelmetProvider } from 'react-helmet-async';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Decorator } from '@storybook/react';
|
||||
|
||||
@ -29,11 +30,13 @@ export const PageDecorator: Decorator<{
|
||||
initialEntries={[computeLocation(args.routePath, args.routeParams)]}
|
||||
>
|
||||
<FullHeightStorybookLayout>
|
||||
<DefaultLayout>
|
||||
<Routes>
|
||||
<Route path={args.routePath} element={<Story />} />
|
||||
</Routes>
|
||||
</DefaultLayout>
|
||||
<HelmetProvider>
|
||||
<DefaultLayout>
|
||||
<Routes>
|
||||
<Route path={args.routePath} element={<Story />} />
|
||||
</Routes>
|
||||
</DefaultLayout>
|
||||
</HelmetProvider>
|
||||
</FullHeightStorybookLayout>
|
||||
</MemoryRouter>
|
||||
</ClientConfigProvider>
|
||||
|
||||
38
front/src/utils/title-utils.ts
Normal file
38
front/src/utils/title-utils.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
|
||||
export function getPageTitleFromPath(pathname: string): string {
|
||||
switch (pathname) {
|
||||
case AppPath.Verify:
|
||||
return 'Verify';
|
||||
case AppPath.SignIn:
|
||||
return 'Sign In';
|
||||
case AppPath.SignUp:
|
||||
return 'Sign Up';
|
||||
case AppPath.Invite:
|
||||
return 'Invite';
|
||||
case AppPath.CreateWorkspace:
|
||||
return 'Create Workspace';
|
||||
case AppPath.CreateProfile:
|
||||
return 'Create Profile';
|
||||
case AppPath.PeoplePage:
|
||||
return 'People';
|
||||
case AppPath.CompaniesPage:
|
||||
return 'Companies';
|
||||
case AppPath.TasksPage:
|
||||
return 'Tasks';
|
||||
case AppPath.OpportunitiesPage:
|
||||
return 'Opportunities';
|
||||
case `${AppBasePath.Settings}/${SettingsPath.ProfilePage}`:
|
||||
return 'Profile';
|
||||
case `${AppBasePath.Settings}/${SettingsPath.Experience}`:
|
||||
return 'Experience';
|
||||
case `${AppBasePath.Settings}/${SettingsPath.WorkspaceMembersPage}`:
|
||||
return 'Workspace Members';
|
||||
case `${AppBasePath.Settings}/${SettingsPath.Workspace}`:
|
||||
return 'Workspace';
|
||||
default:
|
||||
return 'Twenty';
|
||||
}
|
||||
}
|
||||
@ -5603,6 +5603,13 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-helmet-async@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-helmet-async/-/react-helmet-async-1.0.3.tgz#89d581d6cb129e5357d39d7d1b41313b20523989"
|
||||
integrity sha512-DqbSuZPSHiH1l3XI/y8LbhrAGNh+Bpc9QY4MsYRM1yD4+qhax8bN4DInUMpv/tNyIdjsa+1V8XXmbRx8W5dB0w==
|
||||
dependencies:
|
||||
react-helmet-async "*"
|
||||
|
||||
"@types/react-modal@^3.16.0":
|
||||
version "3.16.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-modal/-/react-modal-3.16.0.tgz#b8d6be10de894139a2ea9f4a2505b1b5d02023df"
|
||||
@ -16074,11 +16081,22 @@ react-fast-compare@3.2.1:
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.1.tgz#53933d9e14f364281d6cba24bfed7a4afb808b5f"
|
||||
integrity sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==
|
||||
|
||||
react-fast-compare@^3.0.1:
|
||||
react-fast-compare@^3.0.1, react-fast-compare@^3.2.0:
|
||||
version "3.2.2"
|
||||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
|
||||
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
|
||||
|
||||
react-helmet-async@*, react-helmet-async@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-helmet-async/-/react-helmet-async-1.3.0.tgz#7bd5bf8c5c69ea9f02f6083f14ce33ef545c222e"
|
||||
integrity sha512-9jZ57/dAn9t3q6hneQS0wukqC2ENOBgMNVEhb/ZG9ZSxUetzVIw4iAmEU38IaVg3QGYauQPhSeUTuIUtFglWpg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
invariant "^2.2.4"
|
||||
prop-types "^15.7.2"
|
||||
react-fast-compare "^3.2.0"
|
||||
shallowequal "^1.1.0"
|
||||
|
||||
react-hook-form@^7.45.1:
|
||||
version "7.45.4"
|
||||
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.45.4.tgz#73d228b704026ae95d7e5f7b207a681b173ec62a"
|
||||
@ -17160,6 +17178,11 @@ shallow-equal@^1.2.1:
|
||||
resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da"
|
||||
integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
|
||||
|
||||
shallowequal@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
|
||||
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
|
||||
|
||||
shebang-command@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
|
||||
Reference in New Issue
Block a user