Просмотр исходного кода

Merge pull request #2820 from weseek/imprv/enhance-link-edit-feature-for-merge

Imprv/enhance link edit feature for merge
Yuki Takei 5 лет назад
Родитель
Сommit
7477f9eeb6

+ 16 - 0
src/client/js/components/Icons/PagePreviewIcon.jsx

@@ -0,0 +1,16 @@
+import React from 'react';
+
+const PagePreviewIcon = () => (
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
+    <defs></defs>
+    <rect width="23" height="23" fillOpacity="0" />
+    <path d="M10.94,20.33H3.4V1.38H8.82V8.82h7.44v1.35a6.16,6.16,0,0,1,1.35.47V6.79L10.85,0H3.4a1.3,1.3,0,0,0-1,.39,1.3,1.3,0,0,0-.39,1v19A1.33,
+  1.33,0,0,0,3.4,21.68h9.84A5.94,5.94,0,0,1,10.94,20.33ZM10.17,1.38h.13l6,6v.11H10.17Z"
+    />
+    <path d="M21.87,22.14,18.75,19a4.74,4.74,0,0,0,1.1-3,4.89,4.89,0,1,0-1.8,3.73l3.11,3.11a.5.5,0,0,0,.35.15.51.51,0,0,0,.36-.15A.5.5,
+  0,0,0,21.87,22.14ZM15,19.57A3.57,3.57,0,1,1,18.59,16,3.58,3.58,0,0,1,15,19.57Z"
+    />
+  </svg>
+);
+
+export default PagePreviewIcon;

+ 1 - 1
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -876,7 +876,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
         <LinkEditModal
           ref={this.linkEditModal}
-          onSave={(link) => { return mlu.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), link) }}
+          onSave={(linkText) => { return mlu.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
         <HandsontableModal
           ref={this.handsontableModal}

+ 270 - 150
src/client/js/components/PageEditor/LinkEditModal.jsx

@@ -5,13 +5,16 @@ import {
   Modal,
   ModalHeader,
   ModalBody,
-  ModalFooter,
+  Popover,
+  PopoverBody,
 } from 'reactstrap';
 
 import { debounce } from 'throttle-debounce';
 
 import path from 'path';
+import validator from 'validator';
 import Preview from './Preview';
+import PagePreviewIcon from '../Icons/PagePreviewIcon';
 
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
@@ -34,7 +37,10 @@ class LinkEditModal extends React.PureComponent {
       labelInputValue: '',
       linkerType: Linker.types.markdownLink,
       markdown: '',
+      previewError: '',
       permalink: '',
+      linkText: '',
+      isPreviewOpen: false,
     };
 
     this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker');
@@ -52,47 +58,78 @@ class LinkEditModal extends React.PureComponent {
     this.generateLink = this.generateLink.bind(this);
     this.renderPreview = this.renderPreview.bind(this);
     this.getRootPath = this.getRootPath.bind(this);
-
-    this.getPreviewDebounced = debounce(200, this.getPreview.bind(this));
+    this.toggleIsPreviewOpen = this.toggleIsPreviewOpen.bind(this);
+    this.generateAndSetPreviewDebounced = debounce(200, this.generateAndSetPreview.bind(this));
   }
 
   componentDidUpdate(prevProps, prevState) {
     const { linkInputValue: prevLinkInputValue } = prevState;
     const { linkInputValue } = this.state;
     if (linkInputValue !== prevLinkInputValue) {
-      this.getPreviewDebounced(linkInputValue);
+      this.generateAndSetPreviewDebounced(linkInputValue);
     }
   }
 
   // defaultMarkdownLink is an instance of Linker
   show(defaultMarkdownLink = null) {
     // if defaultMarkdownLink is null, set default value in inputs.
-    const { label = '' } = defaultMarkdownLink;
-    let { link = '', type = Linker.types.markdownLink } = defaultMarkdownLink;
+    const { label = '', link = '' } = defaultMarkdownLink;
+    let { type = Linker.types.markdownLink } = defaultMarkdownLink;
 
     // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
     if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
       type = Linker.types.markdownLink;
     }
 
-    const url = new URL(link, 'http://example.com');
-    const isUseRelativePath = url.origin === 'http://example.com' && !link.startsWith('/') && link !== '';
-    if (isUseRelativePath) {
-      const rootPath = this.getRootPath(type);
-      link = path.resolve(rootPath, link);
-    }
+    this.parseLinkAndSetState(link, type);
 
     this.setState({
       show: true,
       labelInputValue: label,
-      linkInputValue: link,
       isUsePermanentLink: false,
       permalink: '',
       linkerType: type,
+    });
+  }
+
+  // parse link, link is ...
+  // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga')
+  // case-2. absolute path of this growi's page (ex. '/hoge/fuga')
+  // case-3. relative path of this growi's page (ex. '../fuga', 'hoge')
+  // case-4. external link (ex. 'https://growi.org')
+  // case-5. the others (ex. '')
+  parseLinkAndSetState(link, type) {
+    // create url from link, add dummy origin if link is not valid url.
+    // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
+    // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
+    const url = new URL(link, 'http://example.com');
+    const isUrl = url.origin !== 'http://example.com';
+
+    let isUseRelativePath = false;
+    let reshapedLink = link;
+
+    // if case-1, reshapedLink becomes page path
+    reshapedLink = this.convertUrlToPathIfPageUrl(reshapedLink, url);
+
+    // case-3
+    if (!isUrl && !reshapedLink.startsWith('/') && reshapedLink !== '') {
+      isUseRelativePath = true;
+      const rootPath = this.getRootPath(type);
+      reshapedLink = path.resolve(rootPath, reshapedLink);
+    }
+
+    this.setState({
+      linkInputValue: reshapedLink,
       isUseRelativePath,
     });
   }
 
+  // return path name of link if link is this growi page url, else return original link.
+  convertUrlToPathIfPageUrl(link, url) {
+    // when link is this growi's page url, url.origin === window.location.origin and return path name
+    return url.origin === window.location.origin ? decodeURI(url.pathname) : link;
+  }
+
   cancel() {
     this.hide();
   }
@@ -122,27 +159,73 @@ class LinkEditModal extends React.PureComponent {
   }
 
   renderPreview() {
-    return (
-      <div className="linkedit-preview">
-        <Preview
-          markdown={this.state.markdown}
-        />
-      </div>
-    );
+    if (this.state.markdown !== '') {
+      return (
+        <div className="linkedit-preview">
+          <Preview markdown={this.state.markdown} />
+        </div>
+      );
+    }
+    if (this.state.previewError !== '') {
+      return this.state.previewError;
+    }
+    return 'Page preview here.';
   }
 
-  async getPreview(path) {
+  async generateAndSetPreview(path) {
     let markdown = '';
+    let previewError = '';
     let permalink = '';
-    try {
-      const res = await this.props.appContainer.apiGet('/pages.get', { path });
-      markdown = res.page.revision.body;
-      permalink = `${window.location.origin}/${res.page.id}`;
+
+    if (path.startsWith('/')) {
+      const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
+      const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
+      const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
+
+      try {
+        const { page } = await this.props.appContainer.apiGet('/pages.get', { path: pathWithoutFragment, page_id: pageId });
+        markdown = page.revision.body;
+        // create permanent link only if path isn't permanent link because checkbox for isUsePermanentLink is disabled when permalink is ''.
+        permalink = !isPermanentLink ? `${window.location.origin}/${page.id}` : '';
+      }
+      catch (err) {
+        previewError = err.message;
+      }
+    }
+    this.setState({ markdown, previewError, permalink });
+  }
+
+  renderLinkPreview() {
+    const linker = this.generateLink();
+
+    if (this.isUsePermanentLink && this.permalink != null) {
+      linker.link = this.permalink;
     }
-    catch (err) {
-      markdown = `<div class="alert alert-warning" role="alert"><strong>${err.message}</strong></div>`;
+
+    if (linker.label === '') {
+      linker.label = linker.link;
     }
-    this.setState({ markdown, permalink });
+
+    const linkText = linker.generateMarkdownText();
+    return (
+      <div className="d-flex justify-content-between mb-3">
+        <div className="card card-disabled w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">Markdown</p>
+          <p className="text-center text-truncate text-muted">{linkText}</p>
+        </div>
+        <div className="d-flex align-items-center">
+          <span className="lead mx-3">
+            <i className="fa fa-caret-right"></i>
+          </span>
+        </div>
+        <div className="card w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">HTML</p>
+          <p className="text-center text-truncate">
+            <a href={linker.link}>{linker.label}</a>
+          </p>
+        </div>
+      </div>
+    );
   }
 
   handleChangeTypeahead(selected) {
@@ -174,10 +257,8 @@ class LinkEditModal extends React.PureComponent {
   }
 
   save() {
-    const output = this.generateLink();
-
     if (this.props.onSave != null) {
-      this.props.onSave(output);
+      this.props.onSave(this.state.linkText);
     }
 
     this.hide();
@@ -185,12 +266,7 @@ class LinkEditModal extends React.PureComponent {
 
   generateLink() {
     const {
-      linkInputValue,
-      labelInputValue,
-      linkerType,
-      isUseRelativePath,
-      isUsePermanentLink,
-      permalink,
+      linkInputValue, labelInputValue, linkerType, isUseRelativePath, isUsePermanentLink, permalink,
     } = this.state;
 
     let reshapedLink = linkInputValue;
@@ -199,13 +275,11 @@ class LinkEditModal extends React.PureComponent {
       reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue);
     }
 
-    return new Linker(
-      linkerType,
-      labelInputValue,
-      reshapedLink,
-      isUsePermanentLink,
-      permalink,
-    );
+    if (isUsePermanentLink && permalink != null) {
+      reshapedLink = permalink;
+    }
+
+    return new Linker(linkerType, labelInputValue, reshapedLink);
   }
 
   getRootPath(type) {
@@ -215,124 +289,170 @@ class LinkEditModal extends React.PureComponent {
     return type === Linker.types.markdownLink ? path.dirname(pagePath) : pagePath;
   }
 
+  toggleIsPreviewOpen() {
+    this.setState({ isPreviewOpen: !this.state.isPreviewOpen });
+  }
+
+  renderLinkAndLabelForm() {
+    return (
+      <>
+        <h3 className="grw-modal-head">Set link and label</h3>
+        <form className="form-group">
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">link</span>
+              </div>
+              <SearchTypeahead
+                onChange={this.handleChangeTypeahead}
+                onInputChange={this.handleChangeLinkInput}
+                inputName="link"
+                placeholder="Input page path or URL"
+                keywordOnInit={this.state.linkInputValue}
+              />
+              <div className="input-group-append">
+                <button type="button" id="preview-btn" className="btn btn-info btn-page-preview">
+                  <PagePreviewIcon />
+                </button>
+                <Popover trigger="focus" placement="right" isOpen={this.state.isPreviewOpen} target="preview-btn" toggle={this.toggleIsPreviewOpen}>
+                  <PopoverBody>
+                    {this.renderPreview()}
+                  </PopoverBody>
+                </Popover>
+              </div>
+            </div>
+          </div>
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">label</span>
+              </div>
+              <input
+                type="text"
+                className="form-control"
+                id="label"
+                value={this.state.labelInputValue}
+                onChange={e => this.handleChangeLabelInput(e.target.value)}
+                disabled={this.state.linkerType === Linker.types.growiLink}
+              />
+            </div>
+          </div>
+        </form>
+      </>
+    );
+  }
+
+  renderPathFormatForm() {
+    return (
+      <div className="card well pt-3">
+        <form className="form-group mb-0">
+          <div className="form-group row">
+            <label className="col-sm-3">Path format</label>
+            <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
+              <input
+                className="custom-control-input"
+                id="relativePath"
+                type="checkbox"
+                checked={this.state.isUseRelativePath}
+                onChange={this.toggleIsUseRelativePath}
+                disabled={!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink}
+              />
+              <label className="custom-control-label" htmlFor="relativePath">
+                Use relative path
+              </label>
+            </div>
+            <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
+              <input
+                className="custom-control-input"
+                id="permanentLink"
+                type="checkbox"
+                checked={this.state.isUsePermanentLink}
+                onChange={this.toggleIsUsePamanentLink}
+                disabled={this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink}
+              />
+              <label className="custom-control-label" htmlFor="permanentLink">
+                Use permanent link
+              </label>
+            </div>
+          </div>
+          <div className="form-group row mb-0">
+            <label className="col-sm-3">Notation</label>
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="markdownType"
+                value={Linker.types.markdownLink}
+                checked={this.state.linkerType === Linker.types.markdownLink}
+                onChange={e => this.handleSelecteLinkerType(e.target.value)}
+              />
+              <label className="custom-control-label" htmlFor="markdownType">
+                Markdown
+              </label>
+            </div>
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="growiType"
+                value={Linker.types.growiLink}
+                checked={this.state.linkerType === Linker.types.growiLink}
+                onChange={e => this.handleSelecteLinkerType(e.target.value)}
+              />
+              <label className="custom-control-label" htmlFor="growiType">
+                Growi original
+              </label>
+            </div>
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="pukiwikiType"
+                value={Linker.types.pukiwikiLink}
+                checked={this.state.linkerType === Linker.types.pukiwikiLink}
+                onChange={e => this.handleSelecteLinkerType(e.target.value)}
+              />
+              <label className="custom-control-label" htmlFor="pukiwikiType">
+                Pukiwiki
+              </label>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+
   render() {
     return (
-      <Modal isOpen={this.state.show} toggle={this.cancel} size="lg">
+      <Modal className="link-edit-modal" isOpen={this.state.show} toggle={this.cancel} size="lg">
         <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
           Edit Links
         </ModalHeader>
 
         <ModalBody className="container">
           <div className="row">
-            <div className="col-12 col-lg-6">
-              <form className="form-group">
-                <div className="form-gorup my-3">
-                  <label htmlFor="linkInput">Link</label>
-                  <div className="input-group">
-                    <SearchTypeahead
-                      onChange={this.handleChangeTypeahead}
-                      onInputChange={this.handleChangeLinkInput}
-                      inputName="link"
-                      placeholder="Input page path or URL"
-                      keywordOnInit={this.state.linkInputValue}
-                    />
-                  </div>
-                </div>
-              </form>
-
-              <div className="d-block d-lg-none mb-3 overflow-auto">
-                {this.renderPreview()}
-              </div>
-
-              <div className="card">
-                <div className="card-body">
-                  <form className="form-group">
-                    <div className="form-group btn-group d-flex" role="group" aria-label="type">
-                      <button
-                        type="button"
-                        name={Linker.types.markdownLink}
-                        className={`btn btn-outline-secondary col ${this.state.linkerType === Linker.types.markdownLink && 'active'}`}
-                        onClick={e => this.handleSelecteLinkerType(e.target.name)}
-                      >
-                        Markdown
-                      </button>
-                      <button
-                        type="button"
-                        name={Linker.types.growiLink}
-                        className={`btn btn-outline-secondary col ${this.state.linkerType === Linker.types.growiLink && 'active'}`}
-                        onClick={e => this.handleSelecteLinkerType(e.target.name)}
-                      >
-                        Growi Original
-                      </button>
-                      {this.isApplyPukiwikiLikeLinkerPlugin && (
-                        <button
-                          type="button"
-                          name={Linker.types.pukiwikiLink}
-                          className={`btn btn-outline-secondary col ${this.state.linkerType === Linker.types.pukiwikiLink && 'active'}`}
-                          onClick={e => this.handleSelecteLinkerType(e.target.name)}
-                        >
-                          Pukiwiki
-                        </button>
-                      )}
-                    </div>
-
-                    <div className="form-group">
-                      <label htmlFor="label">Label</label>
-                      <input
-                        type="text"
-                        className="form-control"
-                        id="label"
-                        value={this.state.labelInputValue}
-                        onChange={e => this.handleChangeLabelInput(e.target.value)}
-                        disabled={this.state.linkerType === Linker.types.growiLink}
-                      />
-                    </div>
-                    <div className="form-inline">
-                      <div className="custom-control custom-checkbox custom-checkbox-info">
-                        <input
-                          className="custom-control-input"
-                          id="relativePath"
-                          type="checkbox"
-                          checked={this.state.isUseRelativePath}
-                          disabled={!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink}
-                        />
-                        <label className="custom-control-label" htmlFor="relativePath" onClick={this.toggleIsUseRelativePath}>
-                          Use relative path
-                        </label>
-                      </div>
-                    </div>
-                    <div className="form-inline">
-                      <div className="custom-control custom-checkbox custom-checkbox-info">
-                        <input
-                          className="custom-control-input"
-                          id="permanentLink"
-                          type="checkbox"
-                          checked={this.state.isUsePermanentLink}
-                          disabled={this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink}
-                        />
-                        <label className="custom-control-label" htmlFor="permanentLink" onClick={this.toggleIsUsePamanentLink}>
-                          Use permanent link
-                        </label>
-                      </div>
-                    </div>
-                  </form>
-                </div>
-              </div>
+            <div className="col-12">
+              {this.renderLinkAndLabelForm()}
+              {this.renderPathFormatForm()}
             </div>
-
-            <div className="col d-none d-lg-block pr-0 mr-3 overflow-auto">
-              {this.renderPreview()}
+          </div>
+          <div className="row">
+            <div className="col-12">
+              <h3 className="grw-modal-head">Preview</h3>
+              {this.renderLinkPreview()}
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-12 text-center">
+              <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={this.hide}>
+                Cancel
+              </button>
+              <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={this.save}>
+                Done
+              </button>
             </div>
           </div>
         </ModalBody>
-        <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.hide}>
-            Cancel
-          </button>
-          <button type="submit" className="btn btn-sm btn-primary" onClick={this.save}>
-            Done
-          </button>
-        </ModalFooter>
       </Modal>
     );
   }

+ 3 - 4
src/client/js/components/PageEditor/MarkdownLinkUtil.js

@@ -27,16 +27,15 @@ class MarkdownLinkUtil {
   }
 
   // replace link(link is an instance of Linker)
-  replaceFocusedMarkdownLinkWithEditor(editor, link) {
+  replaceFocusedMarkdownLinkWithEditor(editor, linkText) {
     const curPos = editor.getCursor();
-    const linkStr = link.generateMarkdownText();
     if (!this.isInLink(editor)) {
-      editor.getDoc().replaceSelection(linkStr);
+      editor.getDoc().replaceSelection(linkText);
     }
     else {
       const line = editor.getDoc().getLine(curPos.line);
       const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(line, curPos.ch);
-      editor.getDoc().replaceRange(linkStr, { line: curPos.line, ch: beginningOfLink }, { line: curPos.line, ch: endOfLink });
+      editor.getDoc().replaceRange(linkText, { line: curPos.line, ch: beginningOfLink }, { line: curPos.line, ch: endOfLink });
     }
   }
 

+ 7 - 26
src/client/js/models/Linker.js

@@ -1,17 +1,13 @@
 export default class Linker {
 
   constructor(
-      type,
-      label,
-      link,
-      isUsePermanentLink = false,
-      permalink = '',
+      type = Linker.types.markdownLink,
+      label = '',
+      link = '',
   ) {
     this.type = type;
     this.label = label;
     this.link = link;
-    this.isUsePermanentLink = isUsePermanentLink;
-    this.permalink = permalink;
 
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
   }
@@ -30,25 +26,15 @@ export default class Linker {
   }
 
   generateMarkdownText() {
-    let reshapedLink = this.link;
-
-    if (this.isUsePermanentLink && this.permalink != null) {
-      reshapedLink = this.permalink;
-    }
-
-    if (this.label === '') {
-      this.label = reshapedLink;
-    }
-
     if (this.type === Linker.types.pukiwikiLink) {
-      if (this.label === reshapedLink) return `[[${reshapedLink}]]`;
-      return `[[${this.label}>${reshapedLink}]]`;
+      if (this.label === this.link) return `[[${this.link}]]`;
+      return `[[${this.label}>${this.link}]]`;
     }
     if (this.type === Linker.types.growiLink) {
-      return `[${reshapedLink}]`;
+      return `[${this.link}]`;
     }
     if (this.type === Linker.types.markdownLink) {
-      return `[${this.label}](${reshapedLink})`;
+      return `[${this.label}](${this.link})`;
     }
   }
 
@@ -82,15 +68,10 @@ export default class Linker {
       link = label;
     }
 
-    const isUsePermanentLink = false;
-    const permalink = '';
-
     return new Linker(
       type,
       label,
       link,
-      isUsePermanentLink,
-      permalink,
     );
   }
 

+ 13 - 5
src/client/styles/scss/_linkedit-preview.scss

@@ -1,8 +1,16 @@
-.modal .modal-body .linkedit-preview {
-  height: 0;
-  padding-bottom: 50%;
-
+.linkedit-preview {
   .page-editor-preview-body {
-    overflow-y: unset;
+    max-height: 70vh;
+    padding-top: 0px;
+    margin: 0px -10px 0px -10px;
+    .wiki {
+      overflow-y: scroll;
+    }
   }
 }
+
+// page preview button
+.btn-page-preview svg {
+  width: 18px;
+  height: 18px;
+}

+ 31 - 1
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -57,6 +57,10 @@ textarea.form-control {
   border-right: none;
 }
 
+.input-group input {
+  border-color: $secondary;
+}
+
 /*
  * Dropdown
  */
@@ -81,10 +85,20 @@ textarea.form-control {
 /*
  * Card
  */
-.card:not([class*='bg-']) {
+.card:not([class*='bg-']):not(.well):not(.card-disabled) {
   @extend .bg-dark;
 }
 
+// [TODO] GW-3219 modify common color of well in dark theme, then remove below css.
+.card.well {
+  border-color: $secondary;
+}
+
+.card.card-disabled {
+  background-color: lighten($dark, 10%);
+  border-color: $secondary;
+}
+
 /*
  * Pagination
  */
@@ -251,6 +265,22 @@ body.on-edit {
   }
 }
 
+/*
+ * Popover
+ */
+.popover {
+  background-color: $bgcolor-global;
+  border-color: $secondary;
+  .popover-header {
+    color: $white;
+    background-color: $secondary;
+    border-color: $secondary;
+  }
+  .popover-body {
+    color: inherit;
+  }
+}
+
 /*
  * GROWI HandsontableModal
  */

+ 17 - 0
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -42,6 +42,14 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: darken($bgcolor-global, 5%);
 }
 
+/*
+ * card
+ */
+.card.card-disabled {
+  background-color: darken($bgcolor-card, 5%);
+  border-color: $gray-200;
+}
+
 /*
  * GROWI Login form
  */
@@ -186,6 +194,15 @@ $table-hover-bg: $bgcolor-table-hover;
   }
 }
 
+/*
+ * GROWI Link Edit Modal
+ */
+.link-edit-modal {
+  span i {
+    color: $gray-400;
+  }
+}
+
 /*
  * GROWI HandsontableModal
  */

+ 5 - 0
src/client/styles/scss/theme/_apply-colors.scss

@@ -235,6 +235,11 @@ pre:not(.hljs):not(.CodeMirror-line) {
   fill: $color-editor-icons;
 }
 
+// page preview button in link form
+.btn-page-preview svg {
+  fill: white;
+}
+
 /*
  * Modal
  */