Prettier for View, Image and co.
Summary: Trivial beauty. Reviewed By: sahrens Differential Revision: D6715955 fbshipit-source-id: 3632750591f53d4673a2ce76309a0cc62946524d
This commit is contained in:
parent
bf9cabb03c
commit
a5af841d25
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule View
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -52,17 +53,18 @@ const View = createReactClass({
|
|||
*/
|
||||
viewConfig: {
|
||||
uiViewClassName: 'RCTView',
|
||||
validAttributes: ReactNativeViewAttributes.RCTView
|
||||
validAttributes: ReactNativeViewAttributes.RCTView,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
isInAParentText: PropTypes.bool,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
invariant(
|
||||
!(this.context.isInAParentText && Platform.OS === 'android'),
|
||||
'Nesting of <View> within <Text> is not supported on Android.');
|
||||
'Nesting of <View> within <Text> is not supported on Android.',
|
||||
);
|
||||
|
||||
// WARNING: This method will not be used in production mode as in that mode we
|
||||
// replace wrapper component View with generated native wrapper RCTView. Avoid
|
||||
|
@ -76,17 +78,18 @@ const RCTView = requireNativeComponent('RCTView', View, {
|
|||
nativeOnly: {
|
||||
nativeBackgroundAndroid: true,
|
||||
nativeForegroundAndroid: true,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (__DEV__) {
|
||||
const UIManager = require('UIManager');
|
||||
const viewConfig = UIManager.viewConfigs && UIManager.viewConfigs.RCTView || {};
|
||||
const viewConfig =
|
||||
(UIManager.viewConfigs && UIManager.viewConfigs.RCTView) || {};
|
||||
for (const prop in viewConfig.nativeProps) {
|
||||
const viewAny: any = View; // Appease flow
|
||||
if (!viewAny.propTypes[prop] && !ReactNativeStyleAttributes[prop]) {
|
||||
throw new Error(
|
||||
'View is missing propType for native prop `' + prop + '`'
|
||||
'View is missing propType for native prop `' + prop + '`',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -98,4 +101,4 @@ if (__DEV__) {
|
|||
}
|
||||
|
||||
// No one should depend on the DEV-mode createClass View wrapper.
|
||||
module.exports = ((ViewToExport : any) : typeof RCTView);
|
||||
module.exports = ((ViewToExport: any): typeof RCTView);
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
*
|
||||
* @providesModule AssetRegistry
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
||||
export type PackagerAsset = {
|
||||
+__packager_asset: boolean,
|
||||
+fileSystemLocation: string,
|
||||
|
@ -24,7 +24,6 @@ export type PackagerAsset = {
|
|||
+type: string,
|
||||
};
|
||||
|
||||
|
||||
var assets: Array<PackagerAsset> = [];
|
||||
|
||||
function registerAsset(asset: PackagerAsset): number {
|
||||
|
@ -37,4 +36,4 @@ function getAssetByID(assetId: number): PackagerAsset {
|
|||
return assets[assetId - 1];
|
||||
}
|
||||
|
||||
module.exports = { registerAsset, getAssetByID };
|
||||
module.exports = {registerAsset, getAssetByID};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule AssetSourceResolver
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -19,7 +20,7 @@ export type ResolvedAssetSource = {
|
|||
scale: number,
|
||||
};
|
||||
|
||||
import type { PackagerAsset } from 'AssetRegistry';
|
||||
import type {PackagerAsset} from 'AssetRegistry';
|
||||
|
||||
const PixelRatio = require('PixelRatio');
|
||||
const Platform = require('Platform');
|
||||
|
@ -43,22 +44,18 @@ function getScaledAssetPath(asset): string {
|
|||
function getAssetPathInDrawableFolder(asset): string {
|
||||
var scale = AssetSourceResolver.pickScale(asset.scales, PixelRatio.get());
|
||||
var drawbleFolder = assetPathUtils.getAndroidResourceFolderName(asset, scale);
|
||||
var fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
|
||||
var fileName = assetPathUtils.getAndroidResourceIdentifier(asset);
|
||||
return drawbleFolder + '/' + fileName + '.' + asset.type;
|
||||
}
|
||||
|
||||
class AssetSourceResolver {
|
||||
|
||||
serverUrl: ?string;
|
||||
// where the jsbundle is being run from
|
||||
jsbundleUrl: ?string;
|
||||
// the asset to resolve
|
||||
asset: PackagerAsset;
|
||||
|
||||
constructor(serverUrl: ?string,
|
||||
jsbundleUrl: ?string,
|
||||
asset: PackagerAsset
|
||||
) {
|
||||
constructor(serverUrl: ?string, jsbundleUrl: ?string, asset: PackagerAsset) {
|
||||
this.serverUrl = serverUrl;
|
||||
this.jsbundleUrl = jsbundleUrl;
|
||||
this.asset = asset;
|
||||
|
@ -78,9 +75,9 @@ class AssetSourceResolver {
|
|||
}
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
return this.isLoadedFromFileSystem() ?
|
||||
this.drawableFolderInBundle() :
|
||||
this.resourceIdentifierWithoutScale();
|
||||
return this.isLoadedFromFileSystem()
|
||||
? this.drawableFolderInBundle()
|
||||
: this.resourceIdentifierWithoutScale();
|
||||
} else {
|
||||
return this.scaledAssetURLNearBundle();
|
||||
}
|
||||
|
@ -93,8 +90,12 @@ class AssetSourceResolver {
|
|||
assetServerURL(): ResolvedAssetSource {
|
||||
invariant(!!this.serverUrl, 'need server to load from');
|
||||
return this.fromSource(
|
||||
this.serverUrl + getScaledAssetPath(this.asset) +
|
||||
'?platform=' + Platform.OS + '&hash=' + this.asset.hash
|
||||
this.serverUrl +
|
||||
getScaledAssetPath(this.asset) +
|
||||
'?platform=' +
|
||||
Platform.OS +
|
||||
'&hash=' +
|
||||
this.asset.hash,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -122,8 +123,13 @@ class AssetSourceResolver {
|
|||
* E.g. 'assets_awesomemodule_icon'
|
||||
*/
|
||||
resourceIdentifierWithoutScale(): ResolvedAssetSource {
|
||||
invariant(Platform.OS === 'android', 'resource identifiers work on Android');
|
||||
return this.fromSource(assetPathUtils.getAndroidResourceIdentifier(this.asset));
|
||||
invariant(
|
||||
Platform.OS === 'android',
|
||||
'resource identifiers work on Android',
|
||||
);
|
||||
return this.fromSource(
|
||||
assetPathUtils.getAndroidResourceIdentifier(this.asset),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,9 +139,7 @@ class AssetSourceResolver {
|
|||
*/
|
||||
drawableFolderInBundle(): ResolvedAssetSource {
|
||||
const path = this.jsbundleUrl || 'file://';
|
||||
return this.fromSource(
|
||||
path + getAssetPathInDrawableFolder(this.asset)
|
||||
);
|
||||
return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
|
||||
}
|
||||
|
||||
fromSource(source: string): ResolvedAssetSource {
|
||||
|
@ -161,7 +165,6 @@ class AssetSourceResolver {
|
|||
// in which case we default to 1
|
||||
return scales[scales.length - 1] || 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = AssetSourceResolver;
|
||||
module.exports = AssetSourceResolver;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule Image
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -32,9 +33,7 @@ var merge = require('merge');
|
|||
var requireNativeComponent = require('requireNativeComponent');
|
||||
var resolveAssetSource = require('resolveAssetSource');
|
||||
|
||||
var {
|
||||
ImageLoader,
|
||||
} = NativeModules;
|
||||
var {ImageLoader} = NativeModules;
|
||||
|
||||
let _requestId = 1;
|
||||
function generateRequestId() {
|
||||
|
@ -75,14 +74,16 @@ var ImageViewAttributes = merge(ReactNativeViewAttributes.UIView, {
|
|||
});
|
||||
|
||||
var ViewStyleKeys = new Set(Object.keys(ViewStylePropTypes));
|
||||
var ImageSpecificStyleKeys = new Set(Object.keys(ImageStylePropTypes).filter(x => !ViewStyleKeys.has(x)));
|
||||
var ImageSpecificStyleKeys = new Set(
|
||||
Object.keys(ImageStylePropTypes).filter(x => !ViewStyleKeys.has(x)),
|
||||
);
|
||||
|
||||
var Image = createReactClass({
|
||||
displayName: 'Image',
|
||||
propTypes: {
|
||||
...ViewPropTypes,
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
/**
|
||||
/**
|
||||
* `uri` is a string representing the resource identifier for the image, which
|
||||
* could be an http address, a local file path, or a static image
|
||||
* resource (which should be wrapped in the `require('./path/to/image.png')` function).
|
||||
|
@ -108,11 +109,12 @@ var Image = createReactClass({
|
|||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
headers: PropTypes.objectOf(PropTypes.string),
|
||||
}))
|
||||
}),
|
||||
),
|
||||
]),
|
||||
/**
|
||||
* blurRadius: the blur radius of the blur filter added to the image
|
||||
*/
|
||||
* blurRadius: the blur radius of the blur filter added to the image
|
||||
*/
|
||||
blurRadius: PropTypes.number,
|
||||
/**
|
||||
* similarly to `source`, this property represents the resource used to render
|
||||
|
@ -202,9 +204,12 @@ var Image = createReactClass({
|
|||
.then(function(sizes) {
|
||||
success(sizes.width, sizes.height);
|
||||
})
|
||||
.catch(failure || function() {
|
||||
console.warn('Failed to get size for image: ' + url);
|
||||
});
|
||||
.catch(
|
||||
failure ||
|
||||
function() {
|
||||
console.warn('Failed to get size for image: ' + url);
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -231,7 +236,9 @@ var Image = createReactClass({
|
|||
* @return a mapping from url to cache status, such as "disk" or "memory". If a requested URL is
|
||||
* not in the mapping, it means it's not in the cache.
|
||||
*/
|
||||
async queryCache(urls: Array<string>): Promise<Map<string, 'memory' | 'disk'>> {
|
||||
async queryCache(
|
||||
urls: Array<string>,
|
||||
): Promise<Map<string, 'memory' | 'disk'>> {
|
||||
return await ImageLoader.queryCache(urls);
|
||||
},
|
||||
|
||||
|
@ -255,12 +262,14 @@ var Image = createReactClass({
|
|||
},
|
||||
|
||||
contextTypes: {
|
||||
isInAParentText: PropTypes.bool
|
||||
isInAParentText: PropTypes.bool,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const source = resolveAssetSource(this.props.source);
|
||||
const loadingIndicatorSource = resolveAssetSource(this.props.loadingIndicatorSource);
|
||||
const loadingIndicatorSource = resolveAssetSource(
|
||||
this.props.loadingIndicatorSource,
|
||||
);
|
||||
|
||||
// As opposed to the ios version, here we render `null` when there is no source, source.uri
|
||||
// or source array.
|
||||
|
@ -270,11 +279,15 @@ var Image = createReactClass({
|
|||
}
|
||||
|
||||
if (this.props.src) {
|
||||
console.warn('The <Image> component requires a `source` property rather than `src`.');
|
||||
console.warn(
|
||||
'The <Image> component requires a `source` property rather than `src`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.children) {
|
||||
throw new Error('The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.');
|
||||
throw new Error(
|
||||
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
|
||||
);
|
||||
}
|
||||
|
||||
if (source && (source.uri || Array.isArray(source))) {
|
||||
|
@ -292,20 +305,27 @@ var Image = createReactClass({
|
|||
const {onLoadStart, onLoad, onLoadEnd, onError} = this.props;
|
||||
const nativeProps = merge(this.props, {
|
||||
style,
|
||||
shouldNotifyLoadEvents: !!(onLoadStart || onLoad || onLoadEnd || onError),
|
||||
shouldNotifyLoadEvents: !!(
|
||||
onLoadStart ||
|
||||
onLoad ||
|
||||
onLoadEnd ||
|
||||
onError
|
||||
),
|
||||
src: sources,
|
||||
headers: source.headers,
|
||||
loadingIndicatorSrc: loadingIndicatorSource ? loadingIndicatorSource.uri : null,
|
||||
loadingIndicatorSrc: loadingIndicatorSource
|
||||
? loadingIndicatorSource.uri
|
||||
: null,
|
||||
});
|
||||
|
||||
if (this.context.isInAParentText) {
|
||||
return <RCTTextInlineImage {...nativeProps}/>;
|
||||
return <RCTTextInlineImage {...nativeProps} />;
|
||||
} else {
|
||||
return <RKImage {...nativeProps}/>;
|
||||
return <RKImage {...nativeProps} />;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
@ -323,6 +343,10 @@ var cfg = {
|
|||
},
|
||||
};
|
||||
var RKImage = requireNativeComponent('RCTImageView', Image, cfg);
|
||||
var RCTTextInlineImage = requireNativeComponent('RCTTextInlineImage', Image, cfg);
|
||||
var RCTTextInlineImage = requireNativeComponent(
|
||||
'RCTTextInlineImage',
|
||||
Image,
|
||||
cfg,
|
||||
);
|
||||
|
||||
module.exports = Image;
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule Image
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -190,8 +191,8 @@ const Image = createReactClass({
|
|||
*/
|
||||
accessibilityLabel: PropTypes.node,
|
||||
/**
|
||||
* blurRadius: the blur radius of the blur filter added to the image
|
||||
*/
|
||||
* blurRadius: the blur radius of the blur filter added to the image
|
||||
*/
|
||||
blurRadius: PropTypes.number,
|
||||
/**
|
||||
* When the image is resized, the corners of the size specified
|
||||
|
@ -241,7 +242,13 @@ const Image = createReactClass({
|
|||
* - `repeat`: Repeat the image to cover the frame of the view. The
|
||||
* image will keep it's size and aspect ratio. (iOS only)
|
||||
*/
|
||||
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch', 'repeat', 'center']),
|
||||
resizeMode: PropTypes.oneOf([
|
||||
'cover',
|
||||
'contain',
|
||||
'stretch',
|
||||
'repeat',
|
||||
'center',
|
||||
]),
|
||||
/**
|
||||
* A unique identifier for this element to be used in UI Automation
|
||||
* testing scripts.
|
||||
|
@ -314,9 +321,14 @@ const Image = createReactClass({
|
|||
success: (width: number, height: number) => void,
|
||||
failure?: (error: any) => void,
|
||||
) {
|
||||
ImageViewManager.getSize(uri, success, failure || function() {
|
||||
console.warn('Failed to get size for image: ' + uri);
|
||||
});
|
||||
ImageViewManager.getSize(
|
||||
uri,
|
||||
success,
|
||||
failure ||
|
||||
function() {
|
||||
console.warn('Failed to get size for image: ' + uri);
|
||||
},
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Prefetches a remote image for later use by downloading it to the disk
|
||||
|
@ -345,11 +357,15 @@ const Image = createReactClass({
|
|||
*/
|
||||
viewConfig: {
|
||||
uiViewClassName: 'UIView',
|
||||
validAttributes: ReactNativeViewAttributes.UIView
|
||||
validAttributes: ReactNativeViewAttributes.UIView,
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const source = resolveAssetSource(this.props.source) || { uri: undefined, width: undefined, height: undefined };
|
||||
const source = resolveAssetSource(this.props.source) || {
|
||||
uri: undefined,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
};
|
||||
|
||||
let sources;
|
||||
let style;
|
||||
|
@ -358,7 +374,8 @@ const Image = createReactClass({
|
|||
sources = source;
|
||||
} else {
|
||||
const {width, height, uri} = source;
|
||||
style = flattenStyle([{width, height}, styles.base, this.props.style]) || {};
|
||||
style =
|
||||
flattenStyle([{width, height}, styles.base, this.props.style]) || {};
|
||||
sources = [source];
|
||||
|
||||
if (uri === '') {
|
||||
|
@ -366,15 +383,20 @@ const Image = createReactClass({
|
|||
}
|
||||
}
|
||||
|
||||
const resizeMode = this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
|
||||
const resizeMode =
|
||||
this.props.resizeMode || (style || {}).resizeMode || 'cover'; // Workaround for flow bug t7737108
|
||||
const tintColor = (style || {}).tintColor; // Workaround for flow bug t7737108
|
||||
|
||||
if (this.props.src) {
|
||||
console.warn('The <Image> component requires a `source` property rather than `src`.');
|
||||
console.warn(
|
||||
'The <Image> component requires a `source` property rather than `src`.',
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.children) {
|
||||
throw new Error('The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.');
|
||||
throw new Error(
|
||||
'The <Image> component cannot contain children. If you want to render content on top of the image, consider using the <ImageBackground> component or absolute positioning.',
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule ImageEditor
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -63,7 +64,7 @@ class ImageEditor {
|
|||
uri: string,
|
||||
cropData: ImageCropData,
|
||||
success: (uri: string) => void,
|
||||
failure: (error: Object) => void
|
||||
failure: (error: Object) => void,
|
||||
) {
|
||||
RCTImageEditingManager.cropImage(uri, cropData, success, failure);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule ImageResizeMode
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -38,10 +39,10 @@ var ImageResizeMode = keyMirror({
|
|||
*/
|
||||
stretch: null,
|
||||
/**
|
||||
* center - The image will be scaled down such that it is completely visible,
|
||||
* if bigger than the area of the view.
|
||||
* The image will not be scaled up.
|
||||
*/
|
||||
* center - The image will be scaled down such that it is completely visible,
|
||||
* if bigger than the area of the view.
|
||||
* The image will not be scaled up.
|
||||
*/
|
||||
center: null,
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule ImageSource
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule ImageSourcePropType
|
||||
* @no-flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule ImageStore
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -18,7 +19,7 @@ class ImageStore {
|
|||
* Check if the ImageStore contains image data for the specified URI.
|
||||
* @platform ios
|
||||
*/
|
||||
static hasImageForTag(uri: string, callback: (hasImage: bool) => void) {
|
||||
static hasImageForTag(uri: string, callback: (hasImage: boolean) => void) {
|
||||
if (RCTImageStoreManager.hasImageForTag) {
|
||||
RCTImageStoreManager.hasImageForTag(uri, callback);
|
||||
} else {
|
||||
|
@ -56,7 +57,7 @@ class ImageStore {
|
|||
static addImageFromBase64(
|
||||
base64ImageData: string,
|
||||
success: (uri: string) => void,
|
||||
failure: (error: any) => void
|
||||
failure: (error: any) => void,
|
||||
) {
|
||||
RCTImageStoreManager.addImageFromBase64(base64ImageData, success, failure);
|
||||
}
|
||||
|
@ -75,7 +76,7 @@ class ImageStore {
|
|||
static getBase64ForTag(
|
||||
uri: string,
|
||||
success: (base64ImageData: string) => void,
|
||||
failure: (error: any) => void
|
||||
failure: (error: any) => void,
|
||||
) {
|
||||
RCTImageStoreManager.getBase64ForTag(uri, success, failure);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule ImageStylePropTypes
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -51,7 +52,7 @@ var ImageStylePropTypes = {
|
|||
* http://frescolib.org/docs/rounded-corners-and-circles.html
|
||||
*
|
||||
* @platform android
|
||||
*/
|
||||
*/
|
||||
overlayColor: ReactPropTypes.string,
|
||||
|
||||
// Android-Specific styles
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
*
|
||||
* @providesModule nativeImageSource
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -21,7 +22,7 @@ type SourceSpec = {
|
|||
// http://facebook.github.io/react-native/docs/images.html#why-not-automatically-size-everything
|
||||
width: number,
|
||||
height: number,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* In hybrid apps, use `nativeImageSource` to access images that are already available
|
||||
|
@ -40,7 +41,9 @@ type SourceSpec = {
|
|||
function nativeImageSource(spec: SourceSpec): Object {
|
||||
const uri = Platform.select(spec);
|
||||
if (!uri) {
|
||||
console.warn(`No image name given for ${Platform.OS}: ${JSON.stringify(spec)}`);
|
||||
console.warn(
|
||||
`No image name given for ${Platform.OS}: ${JSON.stringify(spec)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
Loading…
Reference in New Issue