mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-02 12:47:41 +02:00
Mobile: Plugin support: Simplify reporting plugin issues (#10319)
This commit is contained in:
parent
34b265475d
commit
aec77b543c
@ -489,6 +489,7 @@ packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
@ -585,6 +586,7 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
@ -1045,6 +1047,8 @@ packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -469,6 +469,7 @@ packages/app-mobile/components/ActionButton.js
|
||||
packages/app-mobile/components/BackButtonDialogBox.js
|
||||
packages/app-mobile/components/CameraView.js
|
||||
packages/app-mobile/components/CustomButton.js
|
||||
packages/app-mobile/components/DismissibleDialog.js
|
||||
packages/app-mobile/components/Dropdown.test.js
|
||||
packages/app-mobile/components/Dropdown.js
|
||||
packages/app-mobile/components/ExtendedWebView.js
|
||||
@ -565,6 +566,7 @@ packages/app-mobile/components/screens/ConfigScreen/SettingsButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/SettingsToggle.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/configScreenStyles.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/ActionButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/PluginInfoButton.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginBox/index.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.test.js
|
||||
packages/app-mobile/components/screens/ConfigScreen/plugins/PluginStates.js
|
||||
@ -1025,6 +1027,8 @@ packages/lib/services/plugins/defaultPlugins/desktopDefaultPluginsInfo.js
|
||||
packages/lib/services/plugins/reducer.js
|
||||
packages/lib/services/plugins/utils/createViewHandle.js
|
||||
packages/lib/services/plugins/utils/executeSandboxCall.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.test.js
|
||||
packages/lib/services/plugins/utils/getPluginIssueReportUrl.js
|
||||
packages/lib/services/plugins/utils/getPluginNamespacedSettingKey.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingKeyPrefix.js
|
||||
packages/lib/services/plugins/utils/getPluginSettingValue.js
|
||||
|
83
packages/app-mobile/components/DismissibleDialog.tsx
Normal file
83
packages/app-mobile/components/DismissibleDialog.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native';
|
||||
import { IconButton, Surface } from 'react-native-paper';
|
||||
import { themeStyle } from './global-style';
|
||||
import Modal from './Modal';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
visible: boolean;
|
||||
onDismiss: ()=> void;
|
||||
containerStyle?: ViewStyle;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const useStyles = (themeId: number, containerStyle: ViewStyle) => {
|
||||
const windowSize = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
webView: {
|
||||
backgroundColor: 'transparent',
|
||||
display: 'flex',
|
||||
},
|
||||
webViewContainer: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
closeButtonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
dialog: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderRadius: 12,
|
||||
padding: 10,
|
||||
|
||||
height: windowSize.height * 0.9,
|
||||
width: windowSize.width * 0.97,
|
||||
|
||||
// Center
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
|
||||
...containerStyle,
|
||||
},
|
||||
});
|
||||
}, [themeId, windowSize.width, windowSize.height, containerStyle]);
|
||||
};
|
||||
|
||||
const DismissibleDialog: React.FC<Props> = props => {
|
||||
const styles = useStyles(props.themeId, props.containerStyle);
|
||||
|
||||
const closeButton = (
|
||||
<View style={styles.closeButtonContainer}>
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={props.onDismiss}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={props.visible}
|
||||
onDismiss={props.onDismiss}
|
||||
onRequestClose={props.onDismiss}
|
||||
animationType='fade'
|
||||
transparent={true}
|
||||
>
|
||||
<Surface style={styles.dialog} elevation={1}>
|
||||
{closeButton}
|
||||
{props.children}
|
||||
</Surface>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default DismissibleDialog;
|
@ -0,0 +1,115 @@
|
||||
import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import * as React from 'react';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Button, IconButton, List, Portal, Text } from 'react-native-paper';
|
||||
import getPluginIssueReportUrl from '@joplin/lib/services/plugins/utils/getPluginIssueReportUrl';
|
||||
import { Linking, ScrollView, StyleSheet, View } from 'react-native';
|
||||
import DismissibleDialog from '../../../../DismissibleDialog';
|
||||
import openWebsiteForPlugin from '../utils/openWebsiteForPlugin';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
size: number;
|
||||
item: PluginItem;
|
||||
onModalDismiss?: ()=> void;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
aboutPluginContainer: {
|
||||
paddingLeft: 10,
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
},
|
||||
descriptionText: {
|
||||
marginTop: 5,
|
||||
marginBottom: 5,
|
||||
},
|
||||
fraudulentPluginButton: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
});
|
||||
|
||||
const PluginInfoModal: React.FC<Props> = props => {
|
||||
const aboutPlugin = (
|
||||
<View style={styles.aboutPluginContainer}>
|
||||
<Text variant='titleLarge'>{props.item.manifest.name}</Text>
|
||||
<Text variant='bodyLarge'>{props.item.manifest.author ? _('by %s', props.item.manifest.author) : ''}</Text>
|
||||
<Text style={styles.descriptionText}>{props.item.manifest.description ?? _('No description')}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const onAboutPress = useCallback(() => {
|
||||
void openWebsiteForPlugin({ item: props.item });
|
||||
}, [props.item]);
|
||||
|
||||
const reportIssueUrl = useMemo(() => {
|
||||
return getPluginIssueReportUrl(props.item.manifest);
|
||||
}, [props.item]);
|
||||
|
||||
const onReportIssuePress = useCallback(() => {
|
||||
void Linking.openURL(reportIssueUrl);
|
||||
}, [reportIssueUrl]);
|
||||
|
||||
const reportIssueButton = (
|
||||
<List.Item
|
||||
left={props => <List.Icon {...props} icon='bug'/>}
|
||||
title={_('Report an issue')}
|
||||
onPress={onReportIssuePress}
|
||||
/>
|
||||
);
|
||||
|
||||
const onReportFraudulentPress = useCallback(() => {
|
||||
void Linking.openURL('https://github.com/laurent22/joplin/security/advisories/new');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<DismissibleDialog
|
||||
themeId={props.themeId}
|
||||
visible={true}
|
||||
onDismiss={props.onModalDismiss}
|
||||
>
|
||||
<ScrollView>
|
||||
{aboutPlugin}
|
||||
<List.Item
|
||||
left={props => <List.Icon {...props} icon='web'/>}
|
||||
title={_('About')}
|
||||
onPress={onAboutPress}
|
||||
/>
|
||||
{ reportIssueUrl ? reportIssueButton : null }
|
||||
</ScrollView>
|
||||
<Button
|
||||
icon='shield-bug'
|
||||
style={styles.fraudulentPluginButton}
|
||||
onPress={onReportFraudulentPress}
|
||||
>{_('Report fraudulent plugin')}</Button>
|
||||
</DismissibleDialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
const PluginInfoButton: React.FC<Props> = props => {
|
||||
const [showInfoModal, setShowInfoModal] = useState(false);
|
||||
const onInfoButtonPress = useCallback(() => {
|
||||
setShowInfoModal(true);
|
||||
}, []);
|
||||
|
||||
const onModalDismiss = useCallback(() => {
|
||||
setShowInfoModal(false);
|
||||
props.onModalDismiss?.();
|
||||
}, [props.onModalDismiss]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showInfoModal ? <PluginInfoModal {...props} onModalDismiss={onModalDismiss} /> : null}
|
||||
<IconButton
|
||||
size={props.size}
|
||||
icon='information'
|
||||
onPress={onInfoButtonPress}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginInfoButton;
|
@ -6,6 +6,7 @@ import { PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import shim from '@joplin/lib/shim';
|
||||
import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import ActionButton, { PluginCallback } from './ActionButton';
|
||||
import PluginInfoButton from './PluginInfoButton';
|
||||
|
||||
export enum InstallState {
|
||||
NotInstalled,
|
||||
@ -21,6 +22,7 @@ export enum UpdateState {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
item: PluginItem;
|
||||
isCompatible: boolean;
|
||||
|
||||
@ -28,11 +30,11 @@ interface Props {
|
||||
installState?: InstallState;
|
||||
updateState?: UpdateState;
|
||||
|
||||
onAboutPress?: PluginCallback;
|
||||
onInstall?: PluginCallback;
|
||||
onUpdate?: PluginCallback;
|
||||
onDelete?: PluginCallback;
|
||||
onToggle?: PluginCallback;
|
||||
onAboutPress?: PluginCallback;
|
||||
onShowPluginLog?: PluginCallback;
|
||||
}
|
||||
|
||||
@ -113,7 +115,7 @@ const PluginBox: React.FC<Props> = props => {
|
||||
);
|
||||
const disableButton = <ActionButton item={item} onPress={props.onToggle} title={_('Disable')}/>;
|
||||
const enableButton = <ActionButton item={item} onPress={props.onToggle} title={_('Enable')}/>;
|
||||
const aboutButton = <ActionButton icon='web' item={item} onPress={props.onAboutPress} title={_('About')}/>;
|
||||
const aboutButton = <ActionButton item={item} onPress={props.onAboutPress} icon='web' title={_('About')}/>;
|
||||
|
||||
const renderErrorsChip = () => {
|
||||
if (!props.hasErrors) return null;
|
||||
@ -165,6 +167,13 @@ const PluginBox: React.FC<Props> = props => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderRightEdgeButton = (buttonProps: { size: number }) => {
|
||||
// If .onAboutPress is given (e.g. when searching), there's another way to get information
|
||||
// about the plugin. In this case, we don't show the right-side information link.
|
||||
if (props.onAboutPress) return null;
|
||||
return <PluginInfoButton {...buttonProps} themeId={props.themeId} item={props.item}/>;
|
||||
};
|
||||
|
||||
const updateStateIsIdle = props.updateState !== UpdateState.Idle;
|
||||
|
||||
const titleComponent = <>
|
||||
@ -177,6 +186,7 @@ const PluginBox: React.FC<Props> = props => {
|
||||
titleStyle={styles.title}
|
||||
subtitle={manifest.description}
|
||||
left={PluginIcon}
|
||||
right={renderRightEdgeButton}
|
||||
/>
|
||||
<Card.Content>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
|
@ -98,6 +98,7 @@ const PluginStates: React.FC<Props> = props => {
|
||||
installedPluginCards.push(
|
||||
<PluginToggle
|
||||
key={`plugin-${key}`}
|
||||
themeId={props.themeId}
|
||||
pluginId={plugin.id}
|
||||
styles={props.styles}
|
||||
pluginSettings={props.pluginSettings}
|
||||
|
@ -11,6 +11,7 @@ import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
|
||||
interface Props {
|
||||
pluginId: string;
|
||||
themeId: number;
|
||||
styles: ConfigScreenStyles;
|
||||
pluginSettings: string;
|
||||
updatablePluginIds: Record<string, boolean>;
|
||||
@ -91,6 +92,7 @@ const PluginToggle: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<PluginBox
|
||||
themeId={props.themeId}
|
||||
item={pluginItem}
|
||||
isCompatible={isCompatible}
|
||||
hasErrors={plugin.hasErrors}
|
||||
|
@ -10,8 +10,8 @@ import PluginBox, { InstallState } from './PluginBox';
|
||||
import PluginService, { PluginSettings } from '@joplin/lib/services/plugins/PluginService';
|
||||
import useInstallHandler from '@joplin/lib/components/shared/config/plugins/useOnInstallHandler';
|
||||
import { OnPluginSettingChangeEvent, PluginItem } from '@joplin/lib/components/shared/config/plugins/types';
|
||||
import onOpenWebsiteForPluginPress from './utils/openWebsiteForPlugin';
|
||||
import RepositoryApi from '@joplin/lib/services/plugins/RepositoryApi';
|
||||
import openWebsiteForPlugin from './utils/openWebsiteForPlugin';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@ -90,15 +90,16 @@ const PluginSearch: React.FC<Props> = props => {
|
||||
|
||||
return (
|
||||
<PluginBox
|
||||
themeId={props.themeId}
|
||||
key={manifest.id}
|
||||
item={item.item}
|
||||
installState={item.installState}
|
||||
isCompatible={PluginService.instance().isCompatible(manifest)}
|
||||
onInstall={installPlugin}
|
||||
onAboutPress={onOpenWebsiteForPluginPress}
|
||||
onAboutPress={openWebsiteForPlugin}
|
||||
/>
|
||||
);
|
||||
}, [installPlugin]);
|
||||
}, [installPlugin, props.themeId]);
|
||||
|
||||
return (
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
import { PluginHtmlContents, PluginStates, ViewInfo } from '@joplin/lib/services/plugins/reducer';
|
||||
import * as React from 'react';
|
||||
import { Button, IconButton, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||
import { Button, Portal, SegmentedButtons, Text } from 'react-native-paper';
|
||||
import useViewInfos from './hooks/useViewInfos';
|
||||
import WebviewController, { ContainerType } from '@joplin/lib/services/plugins/WebviewController';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
@ -9,13 +9,11 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
|
||||
import { connect } from 'react-redux';
|
||||
import { AppState } from '../../../utils/types';
|
||||
import PluginUserWebView from './PluginUserWebView';
|
||||
import { View, useWindowDimensions, StyleSheet, AccessibilityInfo } from 'react-native';
|
||||
import { View, StyleSheet, AccessibilityInfo } from 'react-native';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import { Theme } from '@joplin/lib/themes/type';
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
import Setting from '@joplin/lib/models/Setting';
|
||||
import { Dispatch } from 'redux';
|
||||
import Modal from '../../../components/Modal';
|
||||
import DismissibleDialog from '../../../components/DismissibleDialog';
|
||||
|
||||
interface Props {
|
||||
themeId: number;
|
||||
@ -27,13 +25,7 @@ interface Props {
|
||||
}
|
||||
|
||||
|
||||
const useStyles = (themeId: number) => {
|
||||
const windowSize = useWindowDimensions();
|
||||
|
||||
return useMemo(() => {
|
||||
const theme: Theme = themeStyle(themeId);
|
||||
|
||||
return StyleSheet.create({
|
||||
const styles = StyleSheet.create({
|
||||
webView: {
|
||||
backgroundColor: 'transparent',
|
||||
display: 'flex',
|
||||
@ -42,25 +34,7 @@ const useStyles = (themeId: number) => {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
},
|
||||
closeButtonContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
dialog: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderRadius: 12,
|
||||
padding: 10,
|
||||
|
||||
height: windowSize.height * 0.9,
|
||||
width: windowSize.width * 0.97,
|
||||
|
||||
// Center
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
},
|
||||
});
|
||||
}, [themeId, windowSize.width, windowSize.height]);
|
||||
};
|
||||
});
|
||||
|
||||
type ButtonInfo = {
|
||||
value: string;
|
||||
@ -141,7 +115,6 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
});
|
||||
}, [viewInfoById]);
|
||||
|
||||
const styles = useStyles(props.themeId);
|
||||
const { selectedTabId, setSelectedTabId } = useSelectedTabId(buttonInfos, viewInfoById);
|
||||
|
||||
const viewInfo = viewInfoById[selectedTabId];
|
||||
@ -194,30 +167,16 @@ const PluginPanelViewer: React.FC<Props> = props => {
|
||||
});
|
||||
}, [props.dispatch]);
|
||||
|
||||
const closeButton = (
|
||||
<View style={styles.closeButtonContainer}>
|
||||
<IconButton
|
||||
icon='close'
|
||||
accessibilityLabel={_('Close')}
|
||||
onPress={onClose}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Modal
|
||||
<DismissibleDialog
|
||||
themeId={props.themeId}
|
||||
visible={props.visible}
|
||||
onDismiss={onClose}
|
||||
onRequestClose={onClose}
|
||||
animationType='fade'
|
||||
transparent={true}
|
||||
containerStyle={styles.dialog}
|
||||
>
|
||||
{closeButton}
|
||||
{renderTabContent()}
|
||||
{renderTabSelector()}
|
||||
</Modal>
|
||||
</DismissibleDialog>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,17 @@
|
||||
import getPluginIssueReportUrl from './getPluginIssueReportUrl';
|
||||
|
||||
describe('getPluginIssueReportUrl', () => {
|
||||
test.each([
|
||||
[{ repository_url: 'http://github.com/laurent22/joplin' }, 'https://github.com/laurent22/joplin/issues'],
|
||||
[{ repository_url: 'https://www.github.com/laurent22/joplin' }, 'https://github.com/laurent22/joplin/issues'],
|
||||
[{ repository_url: 'https://www.github.com/laurent22/joplin.git' }, 'https://github.com/laurent22/joplin/issues'],
|
||||
[{ homepage_url: 'https://www.github.com/laurent22/joplin' }, 'https://github.com/laurent22/joplin/issues'],
|
||||
|
||||
[{ homepage_url: 'https://gitlab.com/laurent22/joplin' }, 'https://gitlab.com/laurent22/joplin/-/issues'],
|
||||
[{ homepage_url: 'https://www.gitlab.com/laurent22/joplin' }, 'https://gitlab.com/laurent22/joplin/-/issues'],
|
||||
|
||||
[{ homepage_url: 'https://example.com/laurent22/joplin' }, null],
|
||||
])('should return the issue URL (case %#)', async (manifest, expectedUrl) => {
|
||||
expect(getPluginIssueReportUrl(manifest)).toBe(expectedUrl);
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import { PluginManifest } from './types';
|
||||
|
||||
type ManifestSlice = Pick<PluginManifest, 'repository_url'|'homepage_url'>;
|
||||
const getPluginIssueReportUrl = (pluginManifest: ManifestSlice): string|null => {
|
||||
const githubUrlExp = /^https?:\/\/(?:www\.)?github\.com\/([^/]+)\/([^/?]+)/;
|
||||
const gitlabUrlExp = /^https?:\/\/(?:www\.)?gitlab\.com\/([^/]+)\/([^/]+)/;
|
||||
|
||||
let githubUrlMatch = null;
|
||||
let gitlabUrlMatch = null;
|
||||
const urls = [pluginManifest.repository_url, pluginManifest.homepage_url].filter(url => !!url);
|
||||
|
||||
for (const url of urls) {
|
||||
githubUrlMatch ??= githubUrlExp.exec(url);
|
||||
gitlabUrlMatch ??= gitlabUrlExp.exec(url);
|
||||
}
|
||||
|
||||
if (githubUrlMatch) {
|
||||
const organization = githubUrlMatch[1];
|
||||
// Some plugins include a trailing .git after the repository name
|
||||
const project = githubUrlMatch[2].replace(/\.git$/, '');
|
||||
return `https://github.com/${organization}/${project}/issues`;
|
||||
} else if (gitlabUrlMatch) {
|
||||
const organization = gitlabUrlMatch[1];
|
||||
const project = gitlabUrlMatch[2];
|
||||
return `https://gitlab.com/${organization}/${project}/-/issues`;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getPluginIssueReportUrl;
|
Loading…
Reference in New Issue
Block a user