Repository: shlinkio/shlink-web-client (ts)

Issue:
Allow to set if a short URL should be crawlable, when consuming Shlink 2.7
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personal if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.

With that said, please fill in the information requested next. More information might be requested once the issue is open.
-->


Pull request:
Feature/crawlable option
Closes #430 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 522ddf62..065c3ce6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 * [#432](https://github.com/shlinkio/shlink-web-client/pull/432) Added support to provide the `servers.json` file inside a a `conf.d` folder.
 * [#440](https://github.com/shlinkio/shlink-web-client/pull/440) Added hint of what visits come potentially from a bot, in the visits table, when consuming Shlink >=2.7.
 * [#431](https://github.com/shlinkio/shlink-web-client/pull/431) Added support to filter out visits from potential bots in visits sections, when consuming Shlink >=2.7.
+* [#430](https://github.com/shlinkio/shlink-web-client/pull/430) Added support to set new and existing short URLs as crawlable, when consuming Shlink >=2.7.
 
 ### Changed
 * *Nothing*
diff --git a/src/short-urls/EditShortUrl.tsx b/src/short-urls/EditShortUrl.tsx
index fdcec096..56151bfa 100644
--- a/src/short-urls/EditShortUrl.tsx
+++ b/src/short-urls/EditShortUrl.tsx
@@ -41,6 +41,7 @@ const getInitialState = (shortUrl?: ShortUrl, settings?: ShortUrlCreationSetting
     validSince: shortUrl.meta.validSince ?? undefined,
     validUntil: shortUrl.meta.validUntil ?? undefined,
     maxVisits: shortUrl.meta.maxVisits ?? undefined,
+    crawlable: shortUrl.crawlable,
     validateUrl,
   };
 };
diff --git a/src/short-urls/ShortUrlForm.tsx b/src/short-urls/ShortUrlForm.tsx
index 51714bbc..e30b6c7a 100644
--- a/src/short-urls/ShortUrlForm.tsx
+++ b/src/short-urls/ShortUrlForm.tsx
@@ -6,6 +6,7 @@ import m from 'moment';
 import classNames from 'classnames';
 import DateInput, { DateInputProps } from '../utils/DateInput';
 import {
+  supportsCrawlableVisits,
   supportsListingDomains,
   supportsSettingShortCodeLength,
   supportsShortUrlTitle,
@@ -20,6 +21,7 @@ import { DomainSelectorProps } from '../domains/DomainSelector';
 import { formatIsoDate } from '../utils/helpers/date';
 import UseExistingIfFoundInfoIcon from './UseExistingIfFoundInfoIcon';
 import { ShortUrlData } from './data';
+import { ShortUrlFormCheckboxGroup } from './helpers/ShortUrlFormCheckboxGroup';
 import './ShortUrlForm.scss';
 
 export type Mode = 'create' | 'create-basic' | 'edit';
@@ -108,7 +110,8 @@ export const ShortUrlForm = (
     'col-sm-12': !showCustomizeCard,
   });
   const showValidateUrl = supportsValidateUrl(selectedServer);
-  const showExtraValidationsCard = showValidateUrl || !isEdit;
+  const showCrawlableControl = supportsCrawlableVisits(selectedServer);
+  const showExtraValidationsCard = showValidateUrl || showCrawlableControl || !isEdit;
 
   return (
     <form className="short-url-form" onSubmit={submit}>
@@ -167,23 +170,24 @@ export const ShortUrlForm = (
           </Row>
 
           {showExtraValidationsCard && (
-            <SimpleCard title="Extra validations" className="mb-3">
-              {!isEdit && (
-                <p>
-                  Make sure the long URL is valid, or ensure an existing short URL is returned if it matches all
-                  provided data.
-                </p>
-              )}
+            <SimpleCard title="Extra checks" className="mb-3">
               {showValidateUrl && (
-                <p>
-                  <Checkbox
-                    inline
-                    checked={shortUrlData.validateUrl}
-                    onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })}
-                  >
-                    Validate URL
-                  </Checkbox>
-                </p>
+                <ShortUrlFormCheckboxGroup
+                  infoTooltip="If checked, Shlink will try to reach the long URL, failing in case it's not publicly accessible."
+                  checked={shortUrlData.validateUrl}
+                  onChange={(validateUrl) => setShortUrlData({ ...shortUrlData, validateUrl })}
+                >
+                  Validate URL
+                </ShortUrlFormCheckboxGroup>
+              )}
+              {showCrawlableControl && (
+                <ShortUrlFormCheckboxGroup
+                  infoTooltip="This short URL will be included in the robots.txt for your Shlink instance, allowing web crawlers (like Google) to index it."
+                  checked={shortUrlData.crawlable}
+                  onChange={(crawlable) => setShortUrlData({ ...shortUrlData, crawlable })}
+                >
+                  Make it crawlable
+                </ShortUrlFormCheckboxGroup>
               )}
               {!isEdit && (
                 <p>
diff --git a/src/short-urls/data/index.ts b/src/short-urls/data/index.ts
index c3e5dfea..341f7e8f 100644
--- a/src/short-urls/data/index.ts
+++ b/src/short-urls/data/index.ts
@@ -9,6 +9,7 @@ export interface EditShortUrlData {
   validUntil?: m.Moment | string | null;
   maxVisits?: number | null;
   validateUrl?: boolean;
+  crawlable?: boolean;
 }
 
 export interface ShortUrlData extends EditShortUrlData {
@@ -29,6 +30,7 @@ export interface ShortUrl {
   tags: string[];
   domain: string | null;
   title?: string | null;
+  crawlable?: boolean;
 }
 
 export interface ShortUrlMeta {
diff --git a/src/short-urls/helpers/ShortUrlFormCheckboxGroup.tsx b/src/short-urls/helpers/ShortUrlFormCheckboxGroup.tsx
new file mode 100644
index 00000000..91dff0bc
--- /dev/null
+++ b/src/short-urls/helpers/ShortUrlFormCheckboxGroup.tsx
@@ -0,0 +1,39 @@
+import { ChangeEvent, FC, useRef } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { faInfoCircle as infoIcon } from '@fortawesome/free-solid-svg-icons';
+import { UncontrolledTooltip } from 'reactstrap';
+import Checkbox from '../../utils/Checkbox';
+
+interface ShortUrlFormCheckboxGroupProps {
+  checked?: boolean;
+  onChange?: (checked: boolean, e: ChangeEvent<HTMLInputElement>) => void;
+  infoTooltip?: string;
+}
+
+const InfoTooltip: FC<{ tooltip: string }> = ({ tooltip }) => {
+  const ref = useRef<HTMLElement | null>();
+
+  return (
+    <>
+      <span
+        ref={(el) => {
+          ref.current = el;
+        }}
+      >
+        <FontAwesomeIcon icon={infoIcon} />
+      </span>
+      <UncontrolledTooltip target={(() => ref.current) as any} placement="right">{tooltip}</UncontrolledTooltip>
+    </>
+  );
+};
+
+export const ShortUrlFormCheckboxGroup: FC<ShortUrlFormCheckboxGroupProps> = (
+  { children, infoTooltip, checked, onChange },
+) => (
+  <p>
+    <Checkbox inline checked={checked} className={infoTooltip ? 'mr-2' : ''} onChange={onChange}>
+      {children}
+    </Checkbox>
+    {infoTooltip && <InfoTooltip tooltip={infoTooltip} />}
+  </p>
+);
diff --git a/src/utils/helpers/features.ts b/src/utils/helpers/features.ts
index 4d7633df..9eb314f2 100644
--- a/src/utils/helpers/features.ts
+++ b/src/utils/helpers/features.ts
@@ -25,3 +25,5 @@ export const supportsQrCodeMargin = supportsShortUrlTitle;
 export const supportsTagsInPatch = supportsShortUrlTitle;
 
 export const supportsBotVisits = serverMatchesVersions({ minVersion: '2.7.0' });
+
+export const supportsCrawlableVisits = supportsBotVisits;


Test patch:
diff --git a/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx b/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx
new file mode 100644
index 00000000..76df7496
--- /dev/null
+++ b/test/short-urls/helpers/ShortUrlFormCheckboxGroup.test.tsx
@@ -0,0 +1,16 @@
+import { shallow } from 'enzyme';
+import { ShortUrlFormCheckboxGroup } from '../../../src/short-urls/helpers/ShortUrlFormCheckboxGroup';
+import Checkbox from '../../../src/utils/Checkbox';
+
+describe('<ShortUrlFormCheckboxGroup />', () => {
+  test.each([
+    [ undefined, '', 0 ],
+    [ 'This is the tooltip', 'mr-2', 1 ],
+  ])('renders tooltip only when provided', (infoTooltip, expectedClassName, expectedAmountOfTooltips) => {
+    const wrapper = shallow(<ShortUrlFormCheckboxGroup infoTooltip={infoTooltip} />);
+    const checkbox = wrapper.find(Checkbox);
+
+    expect(checkbox.prop('className')).toEqual(expectedClassName);
+    expect(wrapper.find('InfoTooltip')).toHaveLength(expectedAmountOfTooltips);
+  });
+});