├── .all-contributorsrc ├── .editorconfig ├── .gitignore ├── .npmignore ├── .versionrc.json ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── contexts ├── DataManagerContext.d.ts ├── DataManagerContext.d.ts.map ├── DataManagerContext.js └── DataManagerContext.js.map ├── external-types └── strapi.d.ts ├── hooks ├── useDataManager.d.ts ├── useDataManager.d.ts.map ├── useDataManager.js └── useDataManager.js.map ├── package.json ├── public └── assets │ ├── example1.png │ ├── example2.png │ ├── folder.png │ └── settings.png ├── src ├── admin │ ├── main │ │ ├── clone │ │ │ ├── clone-badge.js │ │ │ └── index.js │ │ ├── index.js │ │ └── preview-context │ │ │ └── index.js │ └── src │ │ ├── components │ │ ├── CodeBlock │ │ │ └── index.js │ │ └── Text │ │ │ └── index.js │ │ ├── containers │ │ ├── Initializer │ │ │ └── index.js │ │ └── SettingsPage │ │ │ ├── Divider.js │ │ │ ├── SectionTitleWrapper.js │ │ │ ├── Wrapper.js │ │ │ ├── index.js │ │ │ ├── init.js │ │ │ └── reducer.js │ │ ├── index.js │ │ ├── lifecycles.js │ │ ├── pluginId.js │ │ ├── translations │ │ ├── ar.json │ │ ├── cs.json │ │ ├── de.json │ │ ├── en.json │ │ ├── es.json │ │ ├── fr.json │ │ ├── id.json │ │ ├── index.js │ │ ├── it.json │ │ ├── ko.json │ │ ├── ms.json │ │ ├── nl.json │ │ ├── pl.json │ │ ├── pt-BR.json │ │ ├── pt.json │ │ ├── ru.json │ │ ├── sk.json │ │ ├── th.json │ │ ├── tr.json │ │ ├── uk.json │ │ ├── vi.json │ │ ├── zh-Hans.json │ │ └── zh.json │ │ └── utils │ │ ├── getRequestUrl.js │ │ ├── getTrad.js │ │ └── index.js ├── config │ ├── functions │ │ └── bootstrap.js │ └── routes.json ├── contexts │ └── DataManagerContext.js ├── controllers │ ├── preview.ts │ └── validations │ │ └── settings.ts ├── hooks │ └── useDataManager.js └── services │ ├── preview-error.ts │ └── preview.ts ├── strapi-files └── v3.6.x │ ├── README.md │ └── extensions │ └── content-manager │ └── admin │ └── src │ ├── components │ └── CustomTable │ │ ├── Row │ │ └── index.js │ │ └── index.js │ └── containers │ ├── EditView │ └── Header │ │ ├── index.js │ │ └── utils │ │ └── connect.js │ └── ListView │ └── index.js ├── tsconfig.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "polcode-dzielonka", 10 | "name": "polcode-dzielonka", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/70939074?v=4", 12 | "profile": "https://github.com/polcode-dzielonka", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "pr0gr8mm3r", 19 | "name": "p_0g_8mm3_", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/37022952?v=4", 21 | "profile": "https://github.com/pr0gr8mm3r", 22 | "contributions": [ 23 | "code" 24 | ] 25 | }, 26 | { 27 | "login": "armaaar", 28 | "name": "Ahmed Rafik Ibrahim", 29 | "avatar_url": "https://avatars.githubusercontent.com/u/25823409?v=4", 30 | "profile": "https://ahmedrafik.me", 31 | "contributions": [ 32 | "code" 33 | ] 34 | } 35 | ], 36 | "contributorsPerLine": 7, 37 | "projectName": "strapi-plugin-preview-content", 38 | "projectOwner": "danestves", 39 | "repoType": "github", 40 | "repoHost": "https://github.com", 41 | "skipCi": true 42 | } 43 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = false 6 | indent_style = space 7 | indent_size = 2 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | public/uploads/* 84 | !public/uploads/.gitkeep 85 | 86 | ############################ 87 | # Node.js 88 | ############################ 89 | 90 | lib-cov 91 | lcov.info 92 | pids 93 | logs 94 | results 95 | node_modules 96 | .node_history 97 | yarn.lock 98 | package-lock.json 99 | 100 | 101 | ############################ 102 | # Tests 103 | ############################ 104 | 105 | testApp 106 | coverage 107 | 108 | ############################ 109 | # Strapi 110 | ############################ 111 | 112 | .env 113 | exports 114 | .cache 115 | build 116 | 117 | 118 | ############################ 119 | # Strapi PLUGIN DIST 120 | ############################ 121 | /config 122 | /controllers 123 | /middlewares 124 | /models 125 | /services 126 | /admin -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | ._* 3 | .DS_Store 4 | .git 5 | .hg 6 | .npmrc 7 | .lock-wscript 8 | .svn 9 | .wafpickle-* 10 | config.gypi 11 | CVS 12 | npm-debug.log 13 | src 14 | __tests__ 15 | coverage 16 | tsconfig.json 17 | dist 18 | external-types -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { "type": "feat", "section": "Features" }, 4 | { "type": "fix", "section": "Bug Fixes" }, 5 | { "type": "chore", "hidden": true }, 6 | { "type": "docs", "hidden": true }, 7 | { "type": "style", "hidden": true }, 8 | { "type": "refactor", "hidden": true }, 9 | { "type": "perf", "hidden": true }, 10 | { "type": "test", "hidden": true } 11 | ], 12 | "commitUrlFormat": "https://github.com/danestves/strapi-plugin-preview-content/commits{{hash}}", 13 | "compareUrlFormat": "https://github.com/danestves/strapi-plugin-preview-content/compare/{{previousTag}}...{{currentTag}}" 14 | } 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #### 1.3.1 (2021-06-23) 2 | 3 | ##### Documentation Changes 4 | 5 | * update .all-contributorsrc [skip ci] ([07a08ab5](https://github.com/danestves/strapi-plugin-preview-content/commit/07a08ab591e3e14bb26463ae778adc0ea1d14f99)) 6 | * update README.md [skip ci] ([513d5109](https://github.com/danestves/strapi-plugin-preview-content/commit/513d510992f1236180af69c16f789afb11d96efa)) 7 | 8 | ##### Bug Fixes 9 | 10 | * pass apiID to CustomTable to show preview button in list view ([9e358202](https://github.com/danestves/strapi-plugin-preview-content/commit/9e358202ed8f13fd4c1ebab443cc9a6f0ee07723)) 11 | * **docs:** deleting custom code from changelog ([4136aeb5](https://github.com/danestves/strapi-plugin-preview-content/commit/4136aeb527ffab51a062b6a1bdde4c401bd6bed5)) 12 | 13 | ### 1.3.0 (2021-06-02) 14 | 15 | ##### New Features 16 | 17 | - **docs:** using generate instead of standard version ([a17a3b54](https://github.com/danestves/strapi-plugin-preview-content/commit/a17a3b54e1db28036f7ca7035204db53647ff632)) 18 | 19 | ### [1.2.1](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.2.0...v1.2.1) (2021-06-02) 20 | 21 | ## [1.2.0](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.1.1...v1.2.0) (2021-06-02) 22 | 23 | ### [1.1.1](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.1.0...v1.1.1) (2021-05-05) 24 | 25 | ## [1.1.0](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.1.0-alpha.0...v1.1.0) (2021-05-05) 26 | 27 | ## [1.1.0-alpha.0](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.0.3-alpha.2...v1.1.0-alpha.0) (2021-05-05) 28 | 29 | ### Features 30 | 31 | - **config:** putting correct config for plugin ([ab54af5](https://github.com/danestves/strapi-plugin-preview-content/commitsab54af587f13340c64d8befcb1c0a0584365af1b)) 32 | 33 | ### [1.0.3-alpha.2](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.0.3-alpha.1...v1.0.3-alpha.2) (2021-05-05) 34 | 35 | ### Bug Fixes 36 | 37 | - **dependencies:** putting react dependency ([1fdf315](https://github.com/danestves/strapi-plugin-preview-content/commits1fdf31566ca87c5fb0555bb0782907fbfb819647)) 38 | 39 | ### [1.0.3-alpha.1](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.0.3-alpha.0...v1.0.3-alpha.1) (2021-05-05) 40 | 41 | ### [1.0.3-alpha.0](https://github.com/danestves/strapi-plugin-preview-content/compare/v1.0.2...v1.0.3-alpha.0) (2021-05-05) 42 | 43 | ### Bug Fixes 44 | 45 | - **compatibility:** files for strapi 3.6.1 ([aadb3a2](https://github.com/danestves/strapi-plugin-preview-content/commitsaadb3a2004c01a07bf5c74d6a0bb6c1a8bb24be5)) 46 | - **compatibility:** files for strapi 3.6.1 ([0183d29](https://github.com/danestves/strapi-plugin-preview-content/commits0183d298f7051f1cca96a512a19df6f5e8a747eb)) 47 | 48 | #### 1.0.2 (2021-05-04) 49 | 50 | ##### Documentation Changes 51 | 52 | - update .all-contributorsrc [skip ci] ([be059e71](https://github.com/danestves/strapi-plugin-preview-content/commit/be059e71d2c131c17d775f3ee7cde3cbc9eaf8c5)) 53 | - update README.md [skip ci] ([9511baed](https://github.com/danestves/strapi-plugin-preview-content/commit/9511baed6ec5ac269a1a02ecdc39608fcf3a69c7)) 54 | - create .all-contributorsrc [skip ci] ([b3dc5499](https://github.com/danestves/strapi-plugin-preview-content/commit/b3dc54998e2dffb61245fcdaf8aee2f67bcf8bda)) 55 | - update README.md [skip ci] ([14beaa03](https://github.com/danestves/strapi-plugin-preview-content/commit/14beaa034ff848c1d7f74acd3da69d1fb804603e)) 56 | 57 | #### 1.0.1 (2021-05-04) 58 | 59 | ##### New Features 60 | 61 | - **docs:** adding supported ([88ccaa5c](https://github.com/danestves/strapi-plugin-preview-content/commit/88ccaa5c35d4635d739445b765e6569ffefe812c)) 62 | 63 | ## 1.0.0 (2021-05-04) 64 | 65 | ##### New Features 66 | 67 | - **docs:** adding changelog generator ([6681560b](https://github.com/danestves/strapi-plugin-preview-content/commit/6681560ba582f211f055581c37662c13492f0f24)) 68 | - add inspired strapi-molecules ([4d63f6cf](https://github.com/danestves/strapi-plugin-preview-content/commit/4d63f6cff197edd69a7864869db97bf5db595786)) 69 | - min width for preview button ([e07c7778](https://github.com/danestves/strapi-plugin-preview-content/commit/e07c77781fbbf348a0663e86f81d87b042262841)) 70 | - updating metadata from package.json ([8112bb1a](https://github.com/danestves/strapi-plugin-preview-content/commit/8112bb1ae2cf1dc69f5ef5c059732f931298718e)) 71 | - version 0.2.7 docs ([0e1d7eec](https://github.com/danestves/strapi-plugin-preview-content/commit/0e1d7eecb0becc76ace6cd04256e1b73fdd57ec7)) 72 | - version 0.2.6 documetation ([87bd3f7d](https://github.com/danestves/strapi-plugin-preview-content/commit/87bd3f7da5982c50745977a5e9d85ded479f4bb0)) 73 | - working with plugins settings ([272e3822](https://github.com/danestves/strapi-plugin-preview-content/commit/272e3822f100d294afc8c71845e33666b64438d5)) 74 | - working with plugins settings ([04539d1f](https://github.com/danestves/strapi-plugin-preview-content/commit/04539d1fda9b13e8840be102927c9acd17e1016f)) 75 | - documentation changelog ([22c2e837](https://github.com/danestves/strapi-plugin-preview-content/commit/22c2e837cceaa2bba578e22b5d54890efef13193)) 76 | - working clone button ([4a081b75](https://github.com/danestves/strapi-plugin-preview-content/commit/4a081b755efbdb70d167fcf0e7292003c62c61c0)) 77 | - working clone button ([6149cf54](https://github.com/danestves/strapi-plugin-preview-content/commit/6149cf54f43300b268a079f2518aff003c5ca8a5)) 78 | - change notifications settings for strapi ([e066cace](https://github.com/danestves/strapi-plugin-preview-content/commit/e066cace928975850d97178676ee1fb9d29f7088)) 79 | - console.log for all entity ([84dc8b05](https://github.com/danestves/strapi-plugin-preview-content/commit/84dc8b05d2c42bf79e49b70573ca71263ae0fdeb)) 80 | - console log to see output data ([a833197a](https://github.com/danestves/strapi-plugin-preview-content/commit/a833197ae4bc31fa11dfd0e5dc838dc031a54f4a)) 81 | - getting preview url from database ([9a2176f8](https://github.com/danestves/strapi-plugin-preview-content/commit/9a2176f8ec01384578d42818aff8f121834ad1f3)) 82 | - missing translations ([c2e25e22](https://github.com/danestves/strapi-plugin-preview-content/commit/c2e25e22c6692a17cc6fd2a595313e29286005ff)) 83 | - add support for models folder ([0ddb702e](https://github.com/danestves/strapi-plugin-preview-content/commit/0ddb702ef399a226f31e98e129c9bb760f49ef8b)) 84 | - strapi 3.4.0 ([669f5358](https://github.com/danestves/strapi-plugin-preview-content/commit/669f5358fc0fa78e3a3494d98481c89d4de92dd6)) 85 | - 1.0.2 ([a1a3a9c9](https://github.com/danestves/strapi-plugin-preview-content/commit/a1a3a9c9f6deddbd934c2f192832c884e0317c31)) 86 | - all with only javascript ([8b099b45](https://github.com/danestves/strapi-plugin-preview-content/commit/8b099b45a2754437a6f9bcd1c0ccb7ec45afafb1)) 87 | - first commit ([9a1ce22b](https://github.com/danestves/strapi-plugin-preview-content/commit/9a1ce22ba26a8ae2526cf2c64b8fb1f8f6e2cc2d)) 88 | 89 | ##### Bug Fixes 90 | 91 | - images not showing in npmjs ([90e0d696](https://github.com/danestves/strapi-plugin-preview-content/commit/90e0d69685cc202baa0b890f2b45df1aef2a38ca)) 92 | - links in docs ([f336b35b](https://github.com/danestves/strapi-plugin-preview-content/commit/f336b35b9005cd42c290631d09c595711983fd16)) 93 | - diff inside README ([b0861967](https://github.com/danestves/strapi-plugin-preview-content/commit/b08619678d0fc3ce9247fc3b1573875e0aac7f31)) 94 | - update or create settings ([be947f5d](https://github.com/danestves/strapi-plugin-preview-content/commit/be947f5d2cdebe4ba1fd36e42b61c0179a59372d)) 95 | - lodash dependency in service ([4bf38ef6](https://github.com/danestves/strapi-plugin-preview-content/commit/4bf38ef68a6fbcd23dce51175012ab8b935ea3ec)) 96 | - packages not needed ([1200b81e](https://github.com/danestves/strapi-plugin-preview-content/commit/1200b81ec46ca5563326dcb508ad39ca00fe6af4)) 97 | - scripts and gitignore ([aaa5bed2](https://github.com/danestves/strapi-plugin-preview-content/commit/aaa5bed212c73c9f6d1998771e8918a93157a222)) 98 | - typescript routes ([d51b9f0a](https://github.com/danestves/strapi-plugin-preview-content/commit/d51b9f0a1fd34bb122e9630a5270f2b06f68d451)) 99 | - empty strapi package ([9ff05cf4](https://github.com/danestves/strapi-plugin-preview-content/commit/9ff05cf454b2ba4a28aacc537210e00c2b52bb5d)) 100 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | estevesd8@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Setting up your dev environment 2 | 3 | ## Step 1: Make a clean strapi installation 4 | 5 | More details on this can be found in [strapi's documentation](https://strapi.io/documentation/developer-docs/latest/getting-started/quick-start.html) 6 | 7 | Add the overriding files to your `extensions` folder, as described in the README. Because the plugin will not be loaded as a package, you will have to replace the following imports: 8 | 9 | | File | Original line | New line | 10 | |---------------------------------------------|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------| 11 | | containers/EditView/Header/index.js | `import { usePreview } from "strapi-plugin-preview-content";` | `import { usePreview } from "/plugins/preview-content/admin/main/preview-context";` | 12 | | containers/EditView/Header/utils/connect.js | `import { PreviewProvider } from "strapi-plugin-preview-content";` | `import { PreviewProvider } from "/plugins/preview-content/admin/main/preview-context";` | 13 | 14 | ## Step 2: Fork this repo 15 | 16 | Hit fork on GitHub so have your own copy of this repository. 17 | 18 | ## Step 3: Clone your fork of the repository into the project 19 | 20 | Git clone your fork into `plugins/preview-content` of your development strapi project. By default, git names the folder `strapi-plugin-preview-content`, so rename it to `preview-content` (or specify the folder name while cloning). 21 | 22 | ## Step 4: Set up the plugin 23 | 24 | To install its dependencies, head into the folder: 25 | 26 | ``` 27 | cd plugins/preview-content 28 | ``` 29 | 30 | and install: 31 | 32 | ``` 33 | yarn install 34 | ``` 35 | 36 | ## Step 5: Run your project with hot reload 37 | 38 | In the `plugins/preview-content` folder run: 39 | 40 | ``` 41 | yarn build -w 42 | ``` 43 | 44 | This will start the typescript compilation with automatic relaoding. 45 | 46 | After the first successful compilation, run strapi in the root of your project: 47 | 48 | ``` 49 | yarn develop --watch-admin 50 | ``` 51 | 52 | # Development 53 | 54 | Be sure to only edit the files in `/plugins/preview-content/src`, as the other files are auto-generated and therefore not tracked. 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Daniel Esteves & Strapi Solutions. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Strapi - Preview Content Plugin 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 |

7 | 8 | NPM Version 9 | 10 | 11 | Monthly download on NPM 12 | 13 |

14 | 15 | > **This plugin has been inspired by [VirtusLab](https://github.com/VirtusLab/) - [strapi-molecules/strapi-plugin-preview](https://github.com/VirtusLab/strapi-molecules/tree/master/packages/strapi-plugin-preview)** 16 | 17 | A plugin for [Strapi Headless CMS](https://github.com/strapi/strapi) that provides content preview to integrate with any frontend: 18 | 19 | This is what the plugin looks like when editing content: 20 | 21 | Example of buttons in edit Content Type 22 | 23 | This is what the plugin looks like when we are in list view: 24 | 25 | Example of buttons in list view 26 | 27 | ### 🖐 Requirements 28 | 29 | Complete installation requirements are exact same as for Strapi itself and can be found in the documentation under Installation Requirements. 30 | 31 | **Supported Strapi versions**: 32 | 33 | - Strapi v3.6.x only version v1.0.0 34 | - Strapi v3.4.x only version v0.2.76 35 | 36 | **We recommend always using the latest version of Strapi to start your new projects**. 37 | 38 | ### ⏳ Installation 39 | 40 | ```bash 41 | # npm 42 | npm install strapi-plugin-preview-content 43 | 44 | # yarn 45 | yarn add strapi-plugin-preview-content 46 | ``` 47 | 48 | ### 📁 Copy required files 49 | 50 | Inside `strapi-files` we have a list of folders with the Strapi version, enter to the version that correspond with your installation, and you will see this files 51 | 52 | Example of buttons in list view 53 | 54 | Copy the folder named `content-manager` inside your `/extensions` folder 55 | 56 | ### 👍 Active content type as previewable 57 | 58 | To enable content type to be previewable and see preview, or clone entry, you've to add option previewable to true in a configuration json file (`*.settings.json`): 59 | 60 | ```diff 61 | { 62 | "pluginOptions": { 63 | + "preview-content": { 64 | + "previewable": true 65 | + } 66 | } 67 | } 68 | ``` 69 | 70 | ### 🚀 Run your project 71 | 72 | After successful installation you've to build a fresh package that includes plugin UI. To archive that simply use: 73 | 74 | ```bash 75 | # npm 76 | npm run build && npm run develop 77 | 78 | # yarn 79 | yarn build && yarn develop 80 | ``` 81 | 82 | ### ✏️ Usage 83 | 84 | Go to Settings > Preview Content 85 | 86 | Preview Content Settings 87 | 88 | #### Base url 89 | 90 | Here you can configure the base url of your frontend. This is a seperate field because it will be different depending on whether your project is running locally (e.g. `http://localhost:3000`) oder in production (e.g. `https://your-site.com`). 91 | 92 | #### Default preview url 93 | 94 | This is the default preview url the plugin uses for when your content type doesn't have its own url defined. For the default preview url there are three parameters provided by this plugin: 95 | 96 | | Parameter | Description | 97 | |-----------------|-----------------------------------| 98 | | `:baseUrl` | See section above for explanation | 99 | | `:contentType` | The content type to query | 100 | | `:id` | The id of content to query | 101 | 102 | For example in NextJS you can make use of [serverless functions](https://nextjs.org/docs/api-routes/introduction) to make an URL like this: 103 | 104 | `:baseUrl/api/preview/:contentType/:id` 105 | 106 | With your base url being `http://localhost:3000`, for example. 107 | 108 | And put the logic there to render content. 109 | 110 | #### Custom preview url 111 | 112 | You can also provide a custom url per content type in its `*.settings.json`. To do so, add this line to its options: 113 | 114 | ```diff 115 | { 116 | "pluginOptions": { 117 | "preview-content": { 118 | "previewable": true, 119 | + "url": ":baseUrl/your-path/:contentType/:id?a-custom-param=true" 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | Here you can see how the base url comes in handy: You cannot change the model in production, but you can change the base url in the settings. 126 | 127 | #### Adding data to the url 128 | 129 | To tell the plugin to allow injection of an entry's data add the following to your model's `*.settings.json`: 130 | 131 | ```diff 132 | { 133 | "pluginOptions": { 134 | "preview-content": { 135 | "previewable": true, 136 | + "url": ":baseUrl/your-path/:contentType/<%= slug %>?a-custom-param=<%= title %>", 137 | + "usesValuesInUrl": true 138 | } 139 | } 140 | } 141 | ``` 142 | 143 | The plugin will now replace `<%= slug %>` and `<%= title %>` with the correct values. What you put in the brackets (`<%=` `%>`) has to be a name of the an attribute. 144 | The syntax used here is [lodash's template syntax](https://lodash.com/docs/4.17.15#template). 145 | 146 | ### ✨ Features 147 | 148 | ### 🛠 API 149 | 150 | There are some functions that make all of this posible 151 | 152 | | function | route | method | description | notes | 153 | | --------------- | ------------------------------- | ------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------ | 154 | | `isPreviewable` | `/is-previewable/:contentType` | `GET` | Get if preview services is active in the current current type | | 155 | | `findOne` | `/:contentType/:id` | `GET` | Find a content type by id | You may want to active this route as public to make request from your frontend | 156 | | `getPreviewUrl` | `/preview-url/:contentType/:id` | `GET` | Get preview url of content type | | 157 | 158 | ## Contributing 159 | 160 | Feel free to fork and make a Pull Request to this plugin project. All the input is warmly welcome! To learn how, head [here](/CONTRIBUTING.md). 161 | 162 | ## Community support 163 | 164 | For general help using Strapi, please refer to [the official Strapi documentation](https://strapi.io/documentation/). For additional help, you can use one of these channels to ask a question: 165 | 166 | - [Slack](http://slack.strapi.io) We're present on official Strapi slack workspace. Look for @danestves and DM. 167 | - [GitHub](https://github.com/danestves/strapi-plugin-preview-content/issues) (Bug reports, Contributions, Questions and Discussions) 168 | 169 | ## License 170 | 171 | [MIT License](LICENSE.md) Copyright (c) 2020 [Daniel Esteves](https://danestves.com/) & [Strapi Solutions](https://strapi.io/). 172 | 173 | ## Contributors ✨ 174 | 175 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 |

polcode-dzielonka

💻

p_0g_8mm3_

💻

Ahmed Rafik Ibrahim

💻
187 | 188 | 189 | 190 | 191 | 192 | 193 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 194 | -------------------------------------------------------------------------------- /contexts/DataManagerContext.d.ts: -------------------------------------------------------------------------------- 1 | export default DataManagerContext; 2 | declare const DataManagerContext: import("react").Context; 3 | //# sourceMappingURL=DataManagerContext.d.ts.map -------------------------------------------------------------------------------- /contexts/DataManagerContext.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"DataManagerContext.d.ts","sourceRoot":"","sources":["../src/contexts/DataManagerContext.js"],"names":[],"mappings":";AAEA,+DAA2C"} -------------------------------------------------------------------------------- /contexts/DataManagerContext.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | var react_1 = require("react"); 4 | var DataManagerContext = react_1.createContext(); 5 | exports.default = DataManagerContext; 6 | //# sourceMappingURL=DataManagerContext.js.map -------------------------------------------------------------------------------- /contexts/DataManagerContext.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"DataManagerContext.js","sourceRoot":"","sources":["../src/contexts/DataManagerContext.js"],"names":[],"mappings":";;AAAA,+BAAsC;AAEtC,IAAM,kBAAkB,GAAG,qBAAa,EAAE,CAAC;AAE3C,kBAAe,kBAAkB,CAAC"} -------------------------------------------------------------------------------- /external-types/strapi.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | declare global { 4 | module NodeJS { 5 | interface Global { 6 | strapi: any; 7 | } 8 | } 9 | } 10 | 11 | declare module "strapi-utils"; 12 | -------------------------------------------------------------------------------- /hooks/useDataManager.d.ts: -------------------------------------------------------------------------------- 1 | export default useDataManager; 2 | declare function useDataManager(): any; 3 | //# sourceMappingURL=useDataManager.d.ts.map -------------------------------------------------------------------------------- /hooks/useDataManager.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useDataManager.d.ts","sourceRoot":"","sources":["../src/hooks/useDataManager.js"],"names":[],"mappings":";AAGA,uCAA2D"} -------------------------------------------------------------------------------- /hooks/useDataManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __importDefault = (this && this.__importDefault) || function (mod) { 3 | return (mod && mod.__esModule) ? mod : { "default": mod }; 4 | }; 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | var react_1 = require("react"); 7 | var DataManagerContext_1 = __importDefault(require("../contexts/DataManagerContext")); 8 | var useDataManager = function () { return react_1.useContext(DataManagerContext_1.default); }; 9 | exports.default = useDataManager; 10 | //# sourceMappingURL=useDataManager.js.map -------------------------------------------------------------------------------- /hooks/useDataManager.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"useDataManager.js","sourceRoot":"","sources":["../src/hooks/useDataManager.js"],"names":[],"mappings":";;;;;AAAA,+BAAmC;AACnC,sFAAgE;AAEhE,IAAM,cAAc,GAAG,cAAM,OAAA,kBAAU,CAAC,4BAAkB,CAAC,EAA9B,CAA8B,CAAC;AAE5D,kBAAe,cAAc,CAAC"} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strapi-plugin-preview-content", 3 | "version": "1.3.1", 4 | "description": "Preview your content with custom URL.", 5 | "main": "admin/main/index.js", 6 | "strapi": { 7 | "name": "Preview Content", 8 | "icon": "link", 9 | "description": "Preview your content with custom URL." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/danestves/strapi-plugin-preview-content.git" 14 | }, 15 | "files": [ 16 | "admin", 17 | "config", 18 | "controllers", 19 | "services" 20 | ], 21 | "scripts": { 22 | "build": "tsc", 23 | "prepublishOnly": "yarn build", 24 | "release:major": "changelog -M && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version major && git push origin && git push origin --tags", 25 | "release:minor": "changelog -m && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version minor && git push origin && git push origin --tags", 26 | "release:patch": "changelog -p && git add CHANGELOG.md && git commit -m 'updated CHANGELOG.md' && npm version patch && git push origin && git push origin --tags" 27 | }, 28 | "dependencies": { 29 | "strapi-helper-plugin": "^3.6.3", 30 | "strapi-utils": "^3.6.3" 31 | }, 32 | "author": { 33 | "name": "Daniel Esteves", 34 | "email": "estevesd8@gmail.com", 35 | "url": "https://danestves.com" 36 | }, 37 | "maintainers": [ 38 | { 39 | "name": "Daniel Esteves", 40 | "email": "estevesd8@gmail.com", 41 | "url": "https://danestves.com" 42 | } 43 | ], 44 | "engines": { 45 | "node": ">=10.16.0 <=14.x.x", 46 | "npm": ">=6.0.0" 47 | }, 48 | "license": "MIT", 49 | "devDependencies": { 50 | "@types/koa": "^2.13.3", 51 | "@types/lodash": "^4.14.170", 52 | "@types/node": "^15.6.2", 53 | "generate-changelog": "^1.8.0", 54 | "typescript": "^4.3.2" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/assets/example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danestves/strapi-plugin-preview-content/c0d5d2416452bd74a6837d097c85ee4492b89720/public/assets/example1.png -------------------------------------------------------------------------------- /public/assets/example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danestves/strapi-plugin-preview-content/c0d5d2416452bd74a6837d097c85ee4492b89720/public/assets/example2.png -------------------------------------------------------------------------------- /public/assets/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danestves/strapi-plugin-preview-content/c0d5d2416452bd74a6837d097c85ee4492b89720/public/assets/folder.png -------------------------------------------------------------------------------- /public/assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danestves/strapi-plugin-preview-content/c0d5d2416452bd74a6837d097c85ee4492b89720/public/assets/settings.png -------------------------------------------------------------------------------- /src/admin/main/clone/clone-badge.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React from "react"; 3 | import { useIntl } from "react-intl"; 4 | import styled from "styled-components"; 5 | 6 | import { Text } from "@buffetjs/core"; 7 | 8 | const Wrapper = styled.div` 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | width: fit-content; 13 | padding: 1rem; 14 | border-radius: 0.2rem; 15 | height: 2.5rem; 16 | ${({ theme }) => ` 17 | border: 1px solid #82b3c9; 18 | background-color: #e1f5fe; 19 | ${Text} { 20 | font-weight: ${theme.main.fontWeights.bold}; 21 | } 22 | `}; 23 | `; 24 | 25 | export const CloneBadge = ({ isClone }) => { 26 | const { formatMessage } = useIntl(); 27 | 28 | if (!isClone) { 29 | return "-"; 30 | } 31 | 32 | return ( 33 | 34 | 35 | {formatMessage({ 36 | id: "preview.containers.List.clone", 37 | })} 38 | 39 | 40 | ); 41 | }; 42 | 43 | CloneBadge.propTypes = { 44 | isClone: PropTypes.bool.isRequired, 45 | }; 46 | -------------------------------------------------------------------------------- /src/admin/main/clone/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { CloneBadge } from "./clone-badge"; 3 | 4 | export const shouldAddCloneHeader = (layout) => { 5 | const { options, attributes } = layout.contentType.schema; 6 | 7 | return options.previewable && !!attributes.cloneOf; 8 | }; 9 | 10 | export const getCloneHeader = (formatMessage) => ({ 11 | label: formatMessage({ id: "preview.containers.List.state" }), 12 | name: "cloneOf", 13 | searchable: false, 14 | sortable: true, 15 | cellFormatter: (cellData) => , 16 | }); 17 | -------------------------------------------------------------------------------- /src/admin/main/index.js: -------------------------------------------------------------------------------- 1 | export * from "./clone"; 2 | export * from "./preview-context"; 3 | -------------------------------------------------------------------------------- /src/admin/main/preview-context/index.js: -------------------------------------------------------------------------------- 1 | import PropTypes from "prop-types"; 2 | import React, { 3 | useState, 4 | useEffect, 5 | useMemo, 6 | useContext, 7 | createContext, 8 | } from "react"; 9 | import { useIntl } from "react-intl"; 10 | import { request, PopUpWarning } from "strapi-helper-plugin"; 11 | 12 | import { get, isEmpty, isEqual } from "lodash"; 13 | 14 | const CONTENT_MANAGER_PLUGIN_ID = "content-manager"; 15 | 16 | const PreviewContext = createContext(undefined); 17 | 18 | export const PreviewProvider = ({ 19 | children, 20 | initialData, 21 | isCreatingEntry, 22 | layout, 23 | modifiedData, 24 | slug, 25 | canUpdate, 26 | canCreate, 27 | getPreviewUrlParams = () => ({}), 28 | }) => { 29 | const { formatMessage } = useIntl(); 30 | 31 | const [showWarningClone, setWarningClone] = useState(false); 32 | const [showWarningPublish, setWarningPublish] = useState(false); 33 | const [previewable, setIsPreviewable] = useState(false); 34 | const [isButtonLoading, setButtonLoading] = useState(false); 35 | 36 | const toggleWarningClone = () => setWarningClone((prevState) => !prevState); 37 | const toggleWarningPublish = () => 38 | setWarningPublish((prevState) => !prevState); 39 | 40 | useEffect(() => { 41 | request(`/preview-content/is-previewable/${layout.apiID}`, { 42 | method: "GET", 43 | }).then(({ isPreviewable }) => { 44 | setIsPreviewable(isPreviewable); 45 | }); 46 | }, [layout.apiID]); 47 | 48 | const didChangeData = useMemo(() => { 49 | return ( 50 | !isEqual(initialData, modifiedData) || 51 | (isCreatingEntry && !isEmpty(modifiedData)) 52 | ); 53 | }, [initialData, isCreatingEntry, modifiedData]); 54 | 55 | const previewHeaderActions = useMemo(() => { 56 | const headerActions = []; 57 | 58 | if ( 59 | previewable && 60 | ((isCreatingEntry && canCreate) || (!isCreatingEntry && canUpdate)) 61 | ) { 62 | const params = getPreviewUrlParams(initialData, modifiedData, layout); 63 | headerActions.push({ 64 | disabled: didChangeData, 65 | label: formatMessage({ 66 | id: getPreviewPluginTrad("containers.Edit.preview"), 67 | }), 68 | color: "secondary", 69 | onClick: async () => { 70 | try { 71 | const data = await request( 72 | `/preview-content/preview-url/${layout.apiID}/${initialData.id}`, 73 | { 74 | method: "GET", 75 | params, 76 | } 77 | ); 78 | 79 | if (data.url) { 80 | window.open(data.url, "_blank"); 81 | } else { 82 | strapi.notification.error( 83 | getPreviewPluginTrad("error.previewUrl.notFound") 84 | ); 85 | } 86 | } catch (_e) { 87 | strapi.notification.error( 88 | getPreviewPluginTrad("error.previewUrl.notFound") 89 | ); 90 | } 91 | }, 92 | type: "button", 93 | style: { 94 | paddingLeft: 15, 95 | paddingRight: 15, 96 | fontWeight: 600, 97 | minWidth: 100, 98 | }, 99 | }); 100 | 101 | if (initialData.cloneOf) { 102 | headerActions.push({ 103 | disabled: didChangeData, 104 | label: formatMessage({ 105 | id: getPreviewPluginTrad("containers.Edit.publish"), 106 | }), 107 | color: "primary", 108 | onClick: async () => { 109 | toggleWarningPublish(); 110 | }, 111 | type: "button", 112 | style: { 113 | paddingLeft: 15, 114 | paddingRight: 15, 115 | fontWeight: 600, 116 | minWidth: 100, 117 | }, 118 | }); 119 | } else { 120 | headerActions.push({ 121 | disabled: didChangeData, 122 | label: formatMessage({ 123 | id: getPreviewPluginTrad("containers.Edit.clone"), 124 | }), 125 | color: "secondary", 126 | onClick: toggleWarningClone, 127 | type: "button", 128 | style: { 129 | paddingLeft: 15, 130 | paddingRight: 15, 131 | fontWeight: 600, 132 | minWidth: 75, 133 | }, 134 | }); 135 | } 136 | } 137 | 138 | return headerActions; 139 | }, [ 140 | didChangeData, 141 | formatMessage, 142 | layout.apiID, 143 | previewable, 144 | initialData.cloneOf, 145 | initialData.id, 146 | canCreate, 147 | canUpdate, 148 | isCreatingEntry, 149 | ]); 150 | 151 | const handleConfirmPreviewClone = async () => { 152 | try { 153 | // Show the loading state 154 | setButtonLoading(true); 155 | 156 | strapi.notification.success(getPreviewPluginTrad("success.record.clone")); 157 | 158 | window.location.replace(getFrontendEntityUrl(slug, initialData.id)); 159 | } catch (err) { 160 | console.log(err); 161 | const errorMessage = get( 162 | err, 163 | "response.payload.message", 164 | formatMessage({ id: getPreviewPluginTrad("error.record.clone") }) 165 | ); 166 | 167 | strapi.notification.error(errorMessage); 168 | } finally { 169 | setButtonLoading(false); 170 | toggleWarningClone(); 171 | } 172 | }; 173 | 174 | const handleConfirmPreviewPublish = async () => { 175 | try { 176 | // Show the loading state 177 | setButtonLoading(true); 178 | 179 | let targetId = initialData.cloneOf.id; 180 | const urlPart = getRequestUrl(slug); 181 | const body = prepareToPublish({ 182 | ...initialData, 183 | id: targetId, 184 | cloneOf: null, 185 | }); 186 | 187 | await request(`${urlPart}/${targetId}`, { 188 | method: "PUT", 189 | body, 190 | }); 191 | await request(`${urlPart}/${initialData.id}`, { 192 | method: "DELETE", 193 | }); 194 | 195 | strapi.notification.success( 196 | getPreviewPluginTrad("success.record.publish") 197 | ); 198 | 199 | window.location.replace(getFrontendEntityUrl(slug, targetId)); 200 | } catch (err) { 201 | const errorMessage = get( 202 | err, 203 | "response.payload.message", 204 | formatMessage({ id: getPreviewPluginTrad("error.record.publish") }) 205 | ); 206 | 207 | strapi.notification.error(errorMessage); 208 | } finally { 209 | setButtonLoading(false); 210 | toggleWarningPublish(); 211 | } 212 | }; 213 | 214 | const value = { 215 | previewHeaderActions, 216 | }; 217 | 218 | return ( 219 | <> 220 | 221 | {children} 222 | 223 | {previewable && ( 224 | 237 | )} 238 | {previewable && ( 239 | 252 | )} 253 | 254 | ); 255 | }; 256 | 257 | export const usePreview = () => { 258 | const context = useContext(PreviewContext); 259 | 260 | if (context === undefined) { 261 | throw new Error("usePreview must be used within a PreviewProvider"); 262 | } 263 | 264 | return context; 265 | }; 266 | 267 | /** 268 | * Should remove ID's from components - 269 | * could modify only already attached componetns (with proper ID) 270 | * or create new one - in that case removing id will create new one 271 | * @param {object} payload 272 | */ 273 | function prepareToPublish(payload) { 274 | if (Array.isArray(payload)) { 275 | payload.forEach(prepareToPublish); 276 | } else if (payload && payload.constructor === Object) { 277 | // eslint-disable-next-line no-prototype-builtins 278 | if (payload.hasOwnProperty("__component")) { 279 | delete payload.id; 280 | } 281 | Object.values(payload).forEach(prepareToPublish); 282 | } 283 | 284 | return payload; 285 | } 286 | 287 | const getRequestUrl = (path) => 288 | `/${CONTENT_MANAGER_PLUGIN_ID}/explorer/${path}`; 289 | const getFrontendEntityUrl = (path, id) => 290 | `/admin/plugins/${CONTENT_MANAGER_PLUGIN_ID}/collectionType/${path}/create/clone/${id}`; 291 | 292 | const getPreviewPluginTrad = (id) => `preview-content.${id}`; 293 | 294 | PreviewProvider.propTypes = { 295 | children: PropTypes.node.isRequired, 296 | canUpdate: PropTypes.bool.isRequired, 297 | canCreate: PropTypes.bool.isRequired, 298 | initialData: PropTypes.object.isRequired, 299 | isCreatingEntry: PropTypes.bool.isRequired, 300 | layout: PropTypes.object.isRequired, 301 | modifiedData: PropTypes.object.isRequired, 302 | slug: PropTypes.string.isRequired, 303 | getPreviewUrlParams: PropTypes.func, 304 | }; 305 | -------------------------------------------------------------------------------- /src/admin/src/components/CodeBlock/index.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import PropTypes from "prop-types"; 3 | 4 | const Text = styled.p` 5 | background: #18202e; 6 | margin: 0; 7 | line-height: ${({ lineHeight }) => lineHeight}; 8 | color: ${({ theme, color }) => theme.main.colors[color] || color}; 9 | font-size: ${({ theme, fontSize }) => theme.main.sizes.fonts[fontSize]}; 10 | font-weight: ${({ theme, fontWeight }) => theme.main.fontWeights[fontWeight]}; 11 | text-transform: ${({ textTransform }) => textTransform}; 12 | ${({ ellipsis }) => 13 | ellipsis && 14 | ` 15 | white-space: nowrap; 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | `} 19 | `; 20 | 21 | Text.defaultProps = { 22 | color: "greyDark", 23 | ellipsis: false, 24 | fontSize: "md", 25 | fontWeight: "regular", 26 | lineHeight: "normal", 27 | textTransform: "none", 28 | }; 29 | 30 | Text.propTypes = { 31 | color: PropTypes.string, 32 | ellipsis: PropTypes.bool, 33 | fontSize: PropTypes.string, 34 | fontWeight: PropTypes.string, 35 | lineHeight: PropTypes.string, 36 | textTransform: PropTypes.string, 37 | }; 38 | 39 | export default Text; 40 | -------------------------------------------------------------------------------- /src/admin/src/components/Text/index.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import PropTypes from "prop-types"; 3 | 4 | const Text = styled.p` 5 | margin: 0; 6 | line-height: ${({ lineHeight }) => lineHeight}; 7 | color: ${({ theme, color }) => theme.main.colors[color] || color}; 8 | font-size: ${({ theme, fontSize }) => theme.main.sizes.fonts[fontSize]}; 9 | font-weight: ${({ theme, fontWeight }) => theme.main.fontWeights[fontWeight]}; 10 | text-transform: ${({ textTransform }) => textTransform}; 11 | ${({ ellipsis }) => 12 | ellipsis && 13 | ` 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | `} 18 | `; 19 | 20 | Text.defaultProps = { 21 | color: "greyDark", 22 | ellipsis: false, 23 | fontSize: "md", 24 | fontWeight: "regular", 25 | lineHeight: "normal", 26 | textTransform: "none", 27 | }; 28 | 29 | Text.propTypes = { 30 | color: PropTypes.string, 31 | ellipsis: PropTypes.bool, 32 | fontSize: PropTypes.string, 33 | fontWeight: PropTypes.string, 34 | lineHeight: PropTypes.string, 35 | textTransform: PropTypes.string, 36 | }; 37 | 38 | export default Text; 39 | -------------------------------------------------------------------------------- /src/admin/src/containers/Initializer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Initializer 4 | * 5 | */ 6 | 7 | import { useEffect, useRef } from "react"; 8 | import PropTypes from "prop-types"; 9 | import pluginId from "../../pluginId"; 10 | 11 | const Initializer = ({ updatePlugin }) => { 12 | const ref = useRef(); 13 | ref.current = updatePlugin; 14 | 15 | useEffect(() => { 16 | ref.current(pluginId, "isReady", true); 17 | }, []); 18 | 19 | return null; 20 | }; 21 | 22 | Initializer.propTypes = { 23 | updatePlugin: PropTypes.func.isRequired, 24 | }; 25 | 26 | export default Initializer; 27 | -------------------------------------------------------------------------------- /src/admin/src/containers/SettingsPage/Divider.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Divider = styled.hr` 4 | height: 1px; 5 | border: 0; 6 | margin-top: 17px; 7 | margin-bottom: 23px; 8 | background: #f6f6f6; 9 | `; 10 | 11 | export default Divider; 12 | -------------------------------------------------------------------------------- /src/admin/src/containers/SettingsPage/SectionTitleWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const SectionTitleWrapper = styled.div` 4 | margin-bottom: 20px; 5 | `; 6 | 7 | export default SectionTitleWrapper; 8 | -------------------------------------------------------------------------------- /src/admin/src/containers/SettingsPage/Wrapper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * This should be a component in the helper plugin that will be used 4 | * by the webhooks views 5 | */ 6 | 7 | import styled from "styled-components"; 8 | 9 | const Wrapper = styled.div` 10 | padding: 25px 10px; 11 | margin-top: 33px; 12 | border-radius: ${({ theme }) => theme.main.sizes.borderRadius}; 13 | box-shadow: 0 2px 4px ${({ theme }) => theme.main.colors.darkGrey}; 14 | background: ${({ theme }) => theme.main.colors.white}; 15 | `; 16 | 17 | export default Wrapper; 18 | -------------------------------------------------------------------------------- /src/admin/src/containers/SettingsPage/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer, useRef } from "react"; 2 | import { Header, Inputs } from "@buffetjs/custom"; 3 | import { 4 | LoadingIndicatorPage, 5 | request, 6 | } from "strapi-helper-plugin"; 7 | import { useIntl } from "react-intl"; 8 | import { isEqual } from "lodash"; 9 | 10 | import { getRequestUrl, getTrad } from "../../utils"; 11 | import Text from "../../components/Text"; 12 | import CodeBlock from "../../components/CodeBlock"; 13 | import SectionTitleWrapper from "./SectionTitleWrapper"; 14 | import Wrapper from "./Wrapper"; 15 | import init from "./init"; 16 | import reducer, { initialState } from "./reducer"; 17 | 18 | const SettingsPage = () => { 19 | const { formatMessage } = useIntl(); 20 | const [reducerState, dispatch] = useReducer(reducer, initialState, init); 21 | const { initialData, isLoading, modifiedData } = reducerState.toJS(); 22 | const isMounted = useRef(true); 23 | const getDataRef = useRef(); 24 | const abortController = new AbortController(); 25 | 26 | getDataRef.current = async () => { 27 | try { 28 | const { signal } = abortController; 29 | const { data } = await request( 30 | getRequestUrl("settings", { method: "GET", signal }) 31 | ); 32 | 33 | if (isMounted.current) { 34 | dispatch({ 35 | type: "GET_DATA_SUCCEEDED", 36 | data, 37 | }); 38 | } 39 | } catch (err) { 40 | console.error(err); 41 | } 42 | }; 43 | 44 | useEffect(() => { 45 | getDataRef.current(); 46 | 47 | return () => { 48 | abortController.abort(); 49 | isMounted.current = false; 50 | }; 51 | // eslint-disable-next-line react-hooks/exhaustive-deps 52 | }, []); 53 | 54 | const handleChange = ({ target: { name, value } }) => { 55 | dispatch({ 56 | type: "ON_CHANGE", 57 | keys: name, 58 | value, 59 | }); 60 | }; 61 | 62 | const handleSubmit = async () => { 63 | try { 64 | await request(getRequestUrl("settings"), { 65 | method: "PUT", 66 | body: modifiedData, 67 | }); 68 | 69 | if (isMounted.current) { 70 | dispatch({ 71 | type: "SUBMIT_SUCCEEDED", 72 | }); 73 | } 74 | 75 | strapi.notification.toggle({ 76 | type: "success", 77 | message: { id: "notification.form.success.fields" }, 78 | }); 79 | } catch (err) { 80 | console.error(err); 81 | } 82 | }; 83 | 84 | const headerProps = { 85 | title: { 86 | label: formatMessage({ id: getTrad("settings.header.label") }), 87 | }, 88 | content: formatMessage({ 89 | id: getTrad("settings.sub-header.label"), 90 | }), 91 | actions: [ 92 | { 93 | color: "cancel", 94 | disabled: isEqual(initialData, modifiedData), 95 | // TradId from the strapi-admin package 96 | label: formatMessage({ id: "app.components.Button.cancel" }), 97 | onClick: () => { 98 | dispatch({ 99 | type: "CANCEL_CHANGES", 100 | }); 101 | }, 102 | type: "button", 103 | }, 104 | { 105 | disabled: false, 106 | color: "success", 107 | // TradId from the strapi-admin package 108 | label: formatMessage({ id: "app.components.Button.save" }), 109 | onClick: handleSubmit, 110 | type: "button", 111 | }, 112 | ], 113 | }; 114 | 115 | if (isLoading) { 116 | return ; 117 | } 118 | 119 | return ( 120 | <> 121 |
122 | 123 |
124 |
125 | 126 | 127 | {formatMessage({ 128 | id: getTrad("settings.section.general.label"), 129 | })} 130 | 131 | 132 |
133 | 145 |
146 |
147 | 159 |
160 |
161 | 162 | {formatMessage({ 163 | id: getTrad("settings.form.previewUrl.available"), 164 | })} 165 | 166 | 167 |
168 | 169 |
170 |
171 | 176 | :baseUrl 177 | 178 | 179 | 180 | {formatMessage({ 181 | id: getTrad( 182 | "settings.form.previewUrl.available.baseUrl" 183 | ), 184 | })} 185 | 186 |
187 |
188 | 193 | :contentType 194 | 195 | 196 | 197 | {formatMessage({ 198 | id: getTrad( 199 | "settings.form.previewUrl.available.contentType" 200 | ), 201 | })} 202 | 203 |
204 |
205 | 210 | :id 211 | 212 | 213 | 214 | {formatMessage({ 215 | id: getTrad("settings.form.previewUrl.available.id"), 216 | })} 217 | 218 |
219 |
220 |
221 |
222 | 223 | {formatMessage({ 224 | id: getTrad("settings.form.previewUrl.example"), 225 | })} 226 | 227 | 228 |
229 | 230 |
231 | 236 | NextJS: {"<"}YOUR_URL{">"} 237 | /api/preview?contentType=:contentType&id=:id 238 | 239 |
240 |
241 |
242 |
243 |
244 | 245 | ); 246 | }; 247 | 248 | export default SettingsPage; 249 | -------------------------------------------------------------------------------- /src/admin/src/containers/SettingsPage/init.js: -------------------------------------------------------------------------------- 1 | const init = (initialState) => { 2 | return initialState; 3 | }; 4 | 5 | export default init; 6 | -------------------------------------------------------------------------------- /src/admin/src/containers/SettingsPage/reducer.js: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | 3 | const initialState = fromJS({ 4 | isLoading: true, 5 | initialData: { 6 | previewUrl: "", 7 | baseUrl: "", 8 | }, 9 | modifiedData: { 10 | previewUrl: "", 11 | baseUrl: "", 12 | }, 13 | }); 14 | 15 | const reducer = (state, action) => { 16 | switch (action.type) { 17 | case "CANCEL_CHANGES": 18 | return state.update("modifiedData", () => state.get("initialData")); 19 | case "GET_DATA_SUCCEEDED": 20 | return state 21 | .update("isLoading", () => false) 22 | .update("initialData", () => fromJS(action.data)) 23 | .update("modifiedData", () => fromJS(action.data)); 24 | case "ON_CHANGE": 25 | return state.updateIn( 26 | ["modifiedData", ...action.keys.split(".")], 27 | () => action.value 28 | ); 29 | case "SUBMIT_SUCCEEDED": 30 | return state.update("initialData", () => state.get("modifiedData")); 31 | default: 32 | return state; 33 | } 34 | }; 35 | 36 | export default reducer; 37 | export { initialState }; 38 | -------------------------------------------------------------------------------- /src/admin/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import pluginPkg from "../../package.json"; 3 | import pluginId from "./pluginId"; 4 | import Initializer from "./containers/Initializer"; 5 | import lifecycles from "./lifecycles"; 6 | import trads from "./translations"; 7 | import SettingsPage from "./containers/SettingsPage"; 8 | 9 | import getTrad from "./utils/getTrad"; 10 | 11 | export default (strapi) => { 12 | const pluginDescription = 13 | pluginPkg.strapi.description || pluginPkg.description; 14 | const icon = pluginPkg.strapi.icon; 15 | const name = pluginPkg.strapi.name; 16 | 17 | const plugin = { 18 | description: pluginDescription, 19 | icon, 20 | id: pluginId, 21 | initializer: Initializer, 22 | isReady: false, 23 | isRequired: pluginPkg.strapi.required || false, 24 | mainComponent: null, 25 | name, 26 | preventComponentRendering: false, 27 | settings: { 28 | global: { 29 | links: [ 30 | { 31 | title: { 32 | id: getTrad("plugin.name"), 33 | defaultMessage: "Preview Content", 34 | }, 35 | name: "preview-content", 36 | to: `${strapi.settingsBaseURL}/preview-content`, 37 | Component: () => , 38 | exact: false, 39 | permissions: [ 40 | { action: "plugins::preview-content.read", subject: null }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | }, 46 | trads, 47 | }; 48 | 49 | return strapi.registerPlugin(plugin); 50 | }; 51 | -------------------------------------------------------------------------------- /src/admin/src/lifecycles.js: -------------------------------------------------------------------------------- 1 | function lifecycles() {} 2 | 3 | export default lifecycles; 4 | -------------------------------------------------------------------------------- /src/admin/src/pluginId.js: -------------------------------------------------------------------------------- 1 | const pluginPkg = require("../../package.json"); 2 | const pluginId = pluginPkg.name.replace(/^strapi-plugin-/i, ""); 3 | 4 | module.exports = pluginId; 5 | -------------------------------------------------------------------------------- /src/admin/src/translations/ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "معاينة المحتوى", 3 | "settings.form.previewUrl.description": "أنشئ عنوان url مخصصًا للمعاينة لاستخدامه في الواجهة الأمامية", 4 | "settings.form.previewUrl.label": "معاينة عنوان URL", 5 | "settings.form.previewUrl.available": "المعلمات المتوفرة:", 6 | "settings.form.previewUrl.available.contentType": "نوع المحتوى المطلوب الاستعلام عنه (مطلوب في عنوان url)", 7 | "settings.form.previewUrl.available.id": "معرف الاستعلام (مطلوب في عنوان url)", 8 | "settings.form.previewUrl.example": "مثال:", 9 | "settings.header.label": "معاينة المحتوى - الإعدادات", 10 | "settings.section.general.label": "عام", 11 | "settings.sub-header.label": "تكوين الإعدادات لوظيفة إعدادات المعاينة" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Náhled obsahu", 3 | "settings.form.previewUrl.description": "Vytvořit vlastní adresu URL náhledu, která bude použita ve vašem rozhraní", 4 | "settings.form.previewUrl.label": "Náhled adresy URL", 5 | "settings.form.previewUrl.available": "Dostupné parametry:", 6 | "settings.form.previewUrl.available.contentType": "Typ obsahu, který má být dotazován (požadovaný v adrese URL)", 7 | "settings.form.previewUrl.available.id": "ID pro dotaz (vyžadováno v adrese URL)", 8 | "settings.form.previewUrl.example": "Příklad:", 9 | "settings.header.label": "Náhled obsahu - nastavení", 10 | "settings.section.general.label": "OBECNÉ", 11 | "settings.sub-header.label": "Konfigurovat nastavení funkce náhledu nastavení" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "containers.Edit.clone": "Duplizieren", 3 | "containers.Edit.preview": "Vorschau", 4 | "containers.Edit.publish": "Veröffentlichen", 5 | 6 | "success.record.clone": "Dupliziert", 7 | "error.record.clone": "Während dem Duplizieren des Eintrags ist ein Fehler aufgetreten.", 8 | "success.record.publish": "Veröffentlicht", 9 | "error.record.publish": "Während dem Veröffentlichen des Eintrags ist ein Fehler aufgetreten.", 10 | 11 | "popUpWarning.warning.clone": "Das Duplizieren legt einen neuen Eintrag mit den selben Feldern an.", 12 | "popUpWarning.warning.clone-question": "Willst du diesen Eintrag duplizieren?", 13 | "popUpWarning.warning.publish": "Diesen Eintrag zu veröffentlichen wird den vorher duplizierten ersetzen (Änderungen werden automatisch auf dem Original-Eintrag angewendet)", 14 | "popUpWarning.warning.publish-question": "Willst du diesen Eintrag wirklich veröffentlichen?", 15 | 16 | "plugin.name": "Vorschau von Inhalten", 17 | "settings.form.previewUrl.description": "Gib eine benutzerdefinierte Vorschau-URL an, die in Ihrem Frontend verwendet werden soll", 18 | "settings.form.previewUrl.label": "Vorschau-URL", 19 | "settings.form.previewUrl.available": "Verfügbare Parameter:", 20 | "settings.form.previewUrl.available.contentType": "Der abzufragende Inhaltstyp (in der URL erforderlich)", 21 | "settings.form.previewUrl.available.id": "Die abzufragende ID (in der URL erforderlich)", 22 | "settings.form.previewUrl.example": "Beispiel:", 23 | "settings.header.label": "Vorschau von Inhalten - Einstellungen", 24 | "settings.section.general.label": "GENERELL", 25 | "settings.sub-header.label": "Konfigurieren Sie die Einstellungen für die Funktion der Vorschaueinstellungen", 26 | 27 | "error.previewUrl.notFound": "Beim Abrufen der URL ist ein Fehler aufgetreten." 28 | } 29 | -------------------------------------------------------------------------------- /src/admin/src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "containers.Edit.clone": "Clone", 3 | "containers.Edit.preview": "Preview", 4 | "containers.Edit.publish": "Publish", 5 | 6 | "success.record.clone": "Cloned", 7 | "error.record.clone": "An error occurred during record clone.", 8 | "success.record.publish": "Published", 9 | "error.record.publish": "An error occurred during record publish.", 10 | 11 | "popUpWarning.warning.clone": "Cloning this entry will create a new copy with the same fields.", 12 | "popUpWarning.warning.clone-question": "Are you sure you want to clone this entry?", 13 | "popUpWarning.warning.publish": "Publishing this entry will replace previously cloned entry (changes will automatically apply to original entry)", 14 | "popUpWarning.warning.publish-question": "Are you sure you want to publish this entry?", 15 | 16 | "plugin.name": "Preview Content", 17 | "settings.form.baseUrl.description": "The root url of your frontend. This can be used by custom and default preview urls.", 18 | "settings.form.baseUrl.label": "Base url", 19 | "settings.form.previewUrl.description": "Create custom preview url to be used in your frontend", 20 | "settings.form.previewUrl.label": "Default Preview url", 21 | "settings.form.previewUrl.available": "Available parameters:", 22 | "settings.form.previewUrl.available.baseUrl": "The root url as defined above", 23 | "settings.form.previewUrl.available.contentType": "The content type to query (required in the url)", 24 | "settings.form.previewUrl.available.id": "The id to query (required in the url)", 25 | "settings.form.previewUrl.example": "Example:", 26 | "settings.header.label": "Preview Content - Settings", 27 | "settings.section.general.label": "GENERAL", 28 | "settings.sub-header.label": "Configure the settings for the preview settings funcionality", 29 | 30 | "error.previewUrl.notFound": "An error occurred during preview url fetch." 31 | } 32 | -------------------------------------------------------------------------------- /src/admin/src/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Vista Previa del Contenido", 3 | "settings.form.previewUrl.description": "Cree una URL de vista previa personalizada para usar en su interfaz", 4 | "settings.form.previewUrl.label": "URL de vista previa", 5 | "settings.form.previewUrl.available": "Parámetros disponibles:", 6 | "settings.form.previewUrl.available.contentType": "El tipo de contenido a consultar (requerido en la URL)", 7 | "settings.form.previewUrl.available.id": "El ID a consultar (requerido en la url)", 8 | "settings.form.previewUrl.example": "Ejemplo:", 9 | "settings.header.label": "Vista Previa del Contenido - Configuración", 10 | "settings.section.general.label": "GENERAL", 11 | "settings.sub-header.label": "Configure los ajustes para la funcionalidad de configuración de vista previa" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Aperçu du contenu", 3 | "settings.form.previewUrl.description": "Créer une URL de prévisualisation personnalisée à utiliser dans votre frontend", 4 | "settings.form.previewUrl.label": "Aperçu de l'url", 5 | "settings.form.previewUrl.available": "Paramètres disponibles:", 6 | "settings.form.previewUrl.available.contentType": "Le type de contenu à interroger (obligatoire dans l'url)", 7 | "settings.form.previewUrl.available.id": "L'identifiant à interroger (obligatoire dans l'url)", 8 | "settings.form.previewUrl.example": "Exemple:", 9 | "settings.header.label": "Aperçu du contenu - Paramètres", 10 | "settings.section.general.label": "GENERAL", 11 | "settings.sub-header.label": "Configurer les paramètres de la fonctionnalité des paramètres de prévisualisation" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Pratinjau Konten", 3 | "settings.form.previewUrl.description": "Buat url pratinjau kustom untuk digunakan di frontend Anda", 4 | "settings.form.previewUrl.label": "Pratinjau url", 5 | "settings.form.previewUrl.available": "Parameter yang tersedia:", 6 | "settings.form.previewUrl.available.contentType": "Jenis konten untuk kueri (diperlukan dalam url)", 7 | "settings.form.previewUrl.available.id": "ID yang akan ditanyakan (harus ada di url)", 8 | "settings.form.previewUrl.example": "Contoh:", 9 | "settings.header.label": "Pratinjau Konten - Pengaturan", 10 | "settings.section.general.label": "GENERAL", 11 | "settings.sub-header.label": "Konfigurasi pengaturan untuk fungsi pengaturan pratinjau" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/index.js: -------------------------------------------------------------------------------- 1 | import ar from './ar.json'; 2 | import cs from './cs.json'; 3 | import de from './de.json'; 4 | import en from './en.json'; 5 | import es from './es.json'; 6 | import fr from './fr.json'; 7 | import id from './id.json'; 8 | import it from './it.json'; 9 | import ko from './ko.json'; 10 | import ms from './ms.json'; 11 | import nl from './nl.json'; 12 | import pl from './pl.json'; 13 | import ptBR from './pt-BR.json'; 14 | import pt from './pt.json'; 15 | import ru from './ru.json'; 16 | import th from './th.json'; 17 | import tr from './tr.json'; 18 | import uk from './uk.json'; 19 | import vi from './vi.json'; 20 | import zhHans from './zh-Hans.json'; 21 | import zh from './zh.json'; 22 | import sk from './sk.json'; 23 | 24 | const trads = { 25 | ar, 26 | cs, 27 | de, 28 | en, 29 | es, 30 | fr, 31 | id, 32 | it, 33 | ko, 34 | ms, 35 | nl, 36 | pl, 37 | 'pt-BR': ptBR, 38 | pt, 39 | ru, 40 | th, 41 | tr, 42 | uk, 43 | vi, 44 | 'zh-Hans': zhHans, 45 | zh, 46 | sk, 47 | }; 48 | 49 | export default trads; 50 | -------------------------------------------------------------------------------- /src/admin/src/translations/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Anteprima contenuto", 3 | "settings.form.previewUrl.description": "Crea un URL di anteprima personalizzato da utilizzare nel tuo frontend", 4 | "settings.form.previewUrl.label": "Anteprima URL", 5 | "settings.form.previewUrl.available": "Parametri disponibili:", 6 | "settings.form.previewUrl.available.contentType": "Il tipo di contenuto da interrogare (obbligatorio nell'URL)", 7 | "settings.form.previewUrl.available.id": "L'ID da interrogare (obbligatorio nell'URL)", 8 | "settings.form.previewUrl.example": "Esempio:", 9 | "settings.header.label": "Anteprima contenuto - Impostazioni", 10 | "settings.section.general.label": "GENERAL", 11 | "settings.sub-header.label": "Configura le impostazioni per la funzionalità delle impostazioni di anteprima" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "콘텐츠 미리보기", 3 | "settings.form.previewUrl.description": "프런트 엔드에서 사용할 맞춤 미리보기 URL 만들기", 4 | "settings.form.previewUrl.label": "URL 미리보기", 5 | "settings.form.previewUrl.available": "사용 가능한 매개 변수 :", 6 | "settings.form.previewUrl.available.contentType": "검색 할 콘텐츠 유형 (URL에 필요)", 7 | "settings.form.previewUrl.available.id": "검색 할 ID (URL에 필요)", 8 | "settings.form.previewUrl.example": "예 :", 9 | "settings.header.label": "콘텐츠 미리보기-설정", 10 | "settings.section.general.label": "일반", 11 | "settings.sub-header.label": "미리보기 설정 기능에 대한 설정 구성" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/ms.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Pratonton Kandungan", 3 | "settings.form.previewUrl.description": "Buat url pratonton khusus untuk digunakan di frontend anda", 4 | "settings.form.previewUrl.label": "Pratonton url", 5 | "settings.form.previewUrl.available": "Parameter yang tersedia:", 6 | "settings.form.previewUrl.available.contentType": "Jenis kandungan untuk pertanyaan (diperlukan dalam url)", 7 | "settings.form.previewUrl.available.id": "Id untuk pertanyaan (diperlukan dalam url)", 8 | "settings.form.previewUrl.example": "Contoh:", 9 | "settings.header.label": "Pratonton Kandungan - Tetapan", 10 | "settings.section.general.label": "UMUM", 11 | "settings.sub-header.label": "Konfigurasikan tetapan untuk fungsi tetapan pratonton" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Voorbeeld van inhoud", 3 | "settings.form.previewUrl.description": "Maak een aangepaste voorbeeld-URL voor gebruik in uw frontend", 4 | "settings.form.previewUrl.label": "Preview url", 5 | "settings.form.previewUrl.available": "Beschikbare parameters:", 6 | "settings.form.previewUrl.available.contentType": "Het inhoudstype dat moet worden opgevraagd (vereist in de url)", 7 | "settings.form.previewUrl.available.id": "De id die moet worden opgevraagd (vereist in de url)", 8 | "settings.form.previewUrl.example": "Voorbeeld:", 9 | "settings.header.label": "Voorbeeld van inhoud - Instellingen", 10 | "settings.section.general.label": "ALGEMEEN", 11 | "settings.sub-header.label": "Configureer de instellingen voor de functionaliteit van de voorbeeldinstellingen" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Podgląd treści", 3 | "settings.form.previewUrl.description": "Utwórz niestandardowy URL podglądu do użycia w interfejsie użytkownika", 4 | "settings.form.previewUrl.label": "URL podglądu", 5 | "settings.form.previewUrl.available": "Dostępne parametry:", 6 | "settings.form.previewUrl.available.contentType": "Typ treści do zapytania (wymagany w url)", 7 | "settings.form.previewUrl.available.id": "Identyfikator do zapytania (wymagany w url)", 8 | "settings.form.previewUrl.example": "Przykład:", 9 | "settings.header.label": "Podgląd treści - ustawienia", 10 | "settings.section.general.label": "OGÓLNE", 11 | "settings.sub-header.label": "Skonfiguruj ustawienia funkcji podglądu" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Visualizar conteúdo", 3 | "settings.form.previewUrl.description": "Crie um URL de visualização personalizado para ser usado em seu frontend", 4 | "settings.form.previewUrl.label": "Visualizar url", 5 | "settings.form.previewUrl.available": "Parâmetros disponíveis:", 6 | "settings.form.previewUrl.available.contentType": "O tipo de conteúdo a ser consultado (obrigatório no url)", 7 | "settings.form.previewUrl.available.id": "O id a consultar (obrigatório no url)", 8 | "settings.form.previewUrl.example": "Exemplo:", 9 | "settings.header.label": "Visualizar conteúdo - Configurações", 10 | "settings.section.general.label": "GERAL", 11 | "settings.sub-header.label": "Definir as configurações para a funcionalidade de configurações de visualização" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Visualizar conteúdo", 3 | "settings.form.previewUrl.description": "Crie um URL de visualização personalizado para ser usado em seu frontend", 4 | "settings.form.previewUrl.label": "Visualizar url", 5 | "settings.form.previewUrl.available": "Parâmetros disponíveis:", 6 | "settings.form.previewUrl.available.contentType": "O tipo de conteúdo a ser consultado (obrigatório no url)", 7 | "settings.form.previewUrl.available.id": "O id a consultar (obrigatório no url)", 8 | "settings.form.previewUrl.example": "Exemplo:", 9 | "settings.header.label": "Visualizar conteúdo - Configurações", 10 | "settings.section.general.label": "GERAL", 11 | "settings.sub-header.label": "Definir as configurações para a funcionalidade de configurações de visualização" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Предварительный просмотр содержимого", 3 | "settings.form.previewUrl.description": "Создайте настраиваемый URL-адрес предварительного просмотра, который будет использоваться в вашем интерфейсе", 4 | "settings.form.previewUrl.label": "URL предварительного просмотра", 5 | "settings.form.previewUrl.available": "Доступные параметры:", 6 | "settings.form.previewUrl.available.contentType": "Тип контента для запроса (требуется в URL-адресе)", 7 | "settings.form.previewUrl.available.id": "Идентификатор запроса (требуется в URL-адресе)", 8 | "settings.form.previewUrl.example": "Пример:", 9 | "settings.header.label": "Предварительный просмотр содержимого - Настройки", 10 | "settings.section.general.label": "GENERAL", 11 | "settings.sub-header.label": "Настроить параметры для функциональности параметров предварительного просмотра" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/sk.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Zobraziť ukážku obsahu", 3 | "settings.form.previewUrl.description": "Vytvoriť vlastnú adresu URL ukážky, ktorá sa použije vo vašom klientskom rozhraní", 4 | "settings.form.previewUrl.label": "Ukážka ukážky", 5 | "settings.form.previewUrl.available": "Dostupné parametre:", 6 | "settings.form.previewUrl.available.contentType": "Typ obsahu, ktorý sa má vyhľadať (požadovaný v adrese URL)", 7 | "settings.form.previewUrl.available.id": "ID na dopyt (požadované v adrese URL)", 8 | "settings.form.previewUrl.example": "Príklad:", 9 | "settings.header.label": "Zobraziť ukážku obsahu - Nastavenia", 10 | "settings.section.general.label": "VŠEOBECNÉ", 11 | "settings.sub-header.label": "Konfigurovať nastavenia funkčnosti nastavenia ukážky" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/th.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "ดูตัวอย่างเนื้อหา", 3 | "settings.form.previewUrl.description": "สร้าง URL ตัวอย่างที่กำหนดเองเพื่อใช้ในส่วนหน้าของคุณ", 4 | "settings.form.previewUrl.label": "แสดงตัวอย่าง url", 5 | "settings.form.previewUrl.available": "พารามิเตอร์ที่มี:", 6 | "settings.form.previewUrl.available.contentType": "ประเภทเนื้อหาที่จะสืบค้น (จำเป็นใน url)", 7 | "settings.form.previewUrl.available.id": "รหัสที่จะค้นหา (จำเป็นใน url)", 8 | "settings.form.previewUrl.example": "ตัวอย่าง:", 9 | "settings.header.label": "ดูตัวอย่างเนื้อหา - การตั้งค่า", 10 | "settings.section.general.label": "GENERAL", 11 | "settings.sub-header.label": "กำหนดการตั้งค่าสำหรับ funcionality การตั้งค่าการแสดงตัวอย่าง" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/tr.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "İçeriği Önizle", 3 | "settings.form.previewUrl.description": "Ön ucunuzda kullanılacak özel önizleme url'si oluşturun", 4 | "settings.form.previewUrl.label": "Önizleme url'si", 5 | "settings.form.previewUrl.available": "Kullanılabilir parametreler:", 6 | "settings.form.previewUrl.available.contentType": "Sorgulanacak içerik türü (url'de gerekli)", 7 | "settings.form.previewUrl.available.id": "Sorgulanacak kimlik (url'de gereklidir)", 8 | "settings.form.previewUrl.example": "Örnek:", 9 | "settings.header.label": "İçeriği Önizle - Ayarlar", 10 | "settings.section.general.label": "GENEL", 11 | "settings.sub-header.label": "Önizleme ayarları işlevselliği için ayarları yapılandırın" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Попередній перегляд вмісту", 3 | "settings.form.previewUrl.description": "Створити користувацьку URL-адресу попереднього перегляду для використання у вашому інтерфейсі", 4 | "settings.form.previewUrl.label": "Попередній перегляд URL-адреси", 5 | "settings.form.previewUrl.available": "Доступні параметри:", 6 | "settings.form.previewUrl.available.contentType": "Тип вмісту для запиту (обов'язково в URL-адресі)", 7 | "settings.form.previewUrl.available.id": "Ідентифікатор для запиту (обов'язковий в URL-адресі)", 8 | "settings.form.previewUrl.example": "Приклад:", 9 | "settings.header.label": "Попередній перегляд вмісту - Налаштування", 10 | "settings.section.general.label": "ЗАГАЛЬНЕ", 11 | "settings.sub-header.label": "Налаштування параметрів функціональності налаштувань попереднього перегляду" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "Xem trước Nội dung", 3 | "settings.form.previewUrl.description": "Tạo url xem trước tùy chỉnh để sử dụng trong giao diện người dùng của bạn", 4 | "settings.form.previewUrl.label": "Url xem trước", 5 | "settings.form.previewUrl.available": "Các thông số có sẵn:", 6 | "settings.form.previewUrl.available.contentType": "Loại nội dung để truy vấn (bắt buộc trong url)", 7 | "settings.form.previewUrl.available.id": "Id để truy vấn (bắt buộc trong url)", 8 | "settings.form.previewUrl.example": "Ví dụ:", 9 | "settings.header.label": "Xem trước Nội dung - Cài đặt", 10 | "settings.section.general.label": "CHUNG", 11 | "settings.sub-header.label": "Định cấu hình cài đặt cho tính năng cá tính của cài đặt xem trước" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/zh-Hans.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "预览内容", 3 | " settings.form.previewUrl.description": "创建要在前端使用的自定义预览网址", 4 | " settings.form.previewUrl.label": "预览网址", 5 | " settings.form.previewUrl.available": "可用参数:", 6 | " settings.form.previewUrl.available.contentType": "要查询的内容类型(在url中要求)", 7 | " settings.form.previewUrl.available.id": "要查询的ID(在URL中必需)", 8 | " settings.form.previewUrl.example": "示例:", 9 | " settings.header.label": "预览内容-设置", 10 | " settings.section.general.label": "一般", 11 | " settings.sub-header.label": "配置预览设置功能的设置" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/translations/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin.name": "預覽內容", 3 | "settings.form.previewUrl.description": "創建要在前端使用的自定義預覽網址", 4 | "settings.form.previewUrl.label": "預覽網址", 5 | "settings.form.previewUrl.available": "可用參數:", 6 | "settings.form.previewUrl.available.contentType": "要查詢的內容類型(在url中要求)", 7 | "settings.form.previewUrl.available.id": "要查詢的ID(在URL中必需)", 8 | "settings.form.previewUrl.example": "示例:", 9 | "settings.header.label": "預覽內容-設置", 10 | "settings.section.general.label": "一般", 11 | "settings.sub-header.label": "配置預覽設置功能的設置" 12 | } 13 | -------------------------------------------------------------------------------- /src/admin/src/utils/getRequestUrl.js: -------------------------------------------------------------------------------- 1 | import pluginId from "../pluginId"; 2 | 3 | const getRequestUrl = (path) => `/${pluginId}/${path}`; 4 | 5 | export default getRequestUrl; 6 | -------------------------------------------------------------------------------- /src/admin/src/utils/getTrad.js: -------------------------------------------------------------------------------- 1 | import pluginId from '../pluginId'; 2 | 3 | const getTrad = id => `${pluginId}.${id}`; 4 | 5 | export default getTrad; 6 | -------------------------------------------------------------------------------- /src/admin/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as getRequestUrl } from "./getRequestUrl"; 2 | export { default as getTrad } from "./getTrad"; 3 | -------------------------------------------------------------------------------- /src/config/functions/bootstrap.js: -------------------------------------------------------------------------------- 1 | module.exports = async () => { 2 | // Add permissions 3 | const actions = [ 4 | { 5 | section: "plugins", 6 | displayName: "Access the Preview", 7 | uid: "read", 8 | pluginName: "preview-content", 9 | }, 10 | ]; 11 | 12 | // set plugin store 13 | const configurator = strapi.store({ 14 | type: "plugin", 15 | name: "preview-content", 16 | key: "settings", 17 | }); 18 | 19 | // if provider config does not exist set one by default 20 | const config = await configurator.get(); 21 | 22 | if (!config) { 23 | await configurator.set({ 24 | value: { 25 | baseUrl: "https://.com", 26 | previewUrl: ":baseUrl/api/preview?contentType=:contentType&id=:id", 27 | }, 28 | }); 29 | } 30 | 31 | const { actionProvider } = global.strapi.admin.services.permission; 32 | 33 | await actionProvider.registerMany(actions); 34 | }; 35 | -------------------------------------------------------------------------------- /src/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "method": "GET", 5 | "path": "/settings", 6 | "handler": "preview.getSettings", 7 | "config": { 8 | "policies": [] 9 | } 10 | }, 11 | { 12 | "method": "PUT", 13 | "path": "/settings", 14 | "handler": "preview.updateSettings", 15 | "config": { 16 | "policies": [] 17 | } 18 | }, 19 | { 20 | "method": "GET", 21 | "path": "/is-previewable/:contentType", 22 | "handler": "preview.isPreviewable", 23 | "config": { 24 | "policies": [] 25 | } 26 | }, 27 | { 28 | "method": "GET", 29 | "path": "/:contentType/:id", 30 | "handler": "preview.findOne", 31 | "config": { 32 | "policies": [] 33 | } 34 | }, 35 | { 36 | "method": "GET", 37 | "path": "/preview-url/:contentType/:id", 38 | "handler": "preview.getPreviewUrl", 39 | "config": { 40 | "policies": [] 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/contexts/DataManagerContext.js: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | const DataManagerContext = createContext(); 4 | 5 | export default DataManagerContext; 6 | -------------------------------------------------------------------------------- /src/controllers/preview.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | import { Context } from "koa"; 3 | import _ from "lodash"; 4 | const validateSettings = require("./validations/settings"); 5 | 6 | /** 7 | * preview.js controller 8 | * 9 | * @description: A set of functions called "actions" of the `preview` plugin. 10 | */ 11 | module.exports = { 12 | /** 13 | * Get if preview services is active 14 | * 15 | * @param ctx 16 | * 17 | * @return Returns true or false for preview 18 | */ 19 | async isPreviewable(ctx: Context) { 20 | const service = global.strapi.plugins["preview-content"].services.preview; 21 | const isPreviewable = await service.isPreviewable(ctx.params.contentType); 22 | 23 | ctx.send({ isPreviewable }); 24 | }, 25 | /** 26 | * Find a content type by id 27 | * 28 | * @param ctx 29 | * 30 | * @returns Returns the content type by id, otherwise null. 31 | */ 32 | async findOne(ctx: Context) { 33 | const service = global.strapi.plugins["preview-content"].services.preview; 34 | const contentPreview = await service.findOne( 35 | ctx.params.contentType, 36 | ctx.params.id, 37 | ctx.query 38 | ); 39 | 40 | ctx.send(contentPreview); 41 | }, 42 | /** 43 | * Get preview url of content type 44 | * 45 | * @param ctx 46 | * 47 | * @returns eturns the object containing the preview url, otherwise null. 48 | */ 49 | async getPreviewUrl(ctx: Context) { 50 | const { 51 | params: { contentType, id }, 52 | query, 53 | } = ctx; 54 | const service = global.strapi.plugins["preview-content"].services.preview; 55 | const url = await service.getPreviewUrl(contentType, id, query); 56 | 57 | ctx.send({ url: url || "" }); 58 | }, 59 | /** 60 | * Get settings of the plugin 61 | */ 62 | async getSettings(ctx: Context) { 63 | // @ts-ignore 64 | const data = await strapi.plugins[ 65 | "preview-content" 66 | ].services.preview.getSettings(); 67 | 68 | ctx.body = { data }; 69 | }, 70 | /** 71 | * Update settings of the plugin 72 | */ 73 | async updateSettings(ctx: Context) { 74 | const { 75 | // @ts-ignore 76 | request: { body }, 77 | } = ctx; 78 | 79 | const data = await validateSettings(body); 80 | 81 | // @ts-ignore 82 | await strapi.plugins["preview-content"].services.preview.setSettings(data); 83 | 84 | ctx.body = { data }; 85 | }, 86 | }; 87 | -------------------------------------------------------------------------------- /src/controllers/validations/settings.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { yup, formatYupErrors } = require("strapi-utils"); 4 | 5 | const settingsSchema = yup.object({ 6 | previewUrl: yup.string().required(), 7 | }); 8 | 9 | const validateSettings = (data: any) => { 10 | return settingsSchema 11 | .validate(data, { 12 | abortEarly: false, 13 | }) 14 | .catch((error: any) => { 15 | // @ts-ignore 16 | throw strapi.errors.badRequest("ValidationError", { 17 | errors: formatYupErrors(error), 18 | }); 19 | }); 20 | }; 21 | 22 | module.exports = validateSettings; 23 | -------------------------------------------------------------------------------- /src/hooks/useDataManager.js: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import DataManagerContext from "../contexts/DataManagerContext"; 3 | 4 | const useDataManager = () => useContext(DataManagerContext); 5 | 6 | export default useDataManager; 7 | -------------------------------------------------------------------------------- /src/services/preview-error.ts: -------------------------------------------------------------------------------- 1 | module.exports = class PreviewError extends ( 2 | Error 3 | ) { 4 | status: number; 5 | payload: any; 6 | constructor(status: number, message: string, payload: any = undefined) { 7 | super(); 8 | this.name = "Strapi:Plugin:PreviewContent"; 9 | this.status = status || 500; 10 | this.message = message || "Internal error"; 11 | this.payload = payload; 12 | } 13 | 14 | toString(e = this) { 15 | return `${e.name} - ${e.message}`; 16 | } 17 | 18 | getData() { 19 | if (this.payload) { 20 | return JSON.stringify({ 21 | name: this.name, 22 | message: this.message, 23 | ...(this.payload || {}), 24 | }); 25 | } 26 | return this.toString(); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/services/preview.ts: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import _ from "lodash"; 4 | 5 | const { sanitizeEntity } = require("strapi-utils"); 6 | 7 | const PreviewError = require("./preview-error"); 8 | 9 | /** 10 | * Get components from a givne template 11 | * 12 | * @param {{ __component: string }[]} template 13 | * 14 | * @returns Returns the component, otherwise a error 400. 15 | */ 16 | // const getTemplateComponentFromTemplate = (template) => { 17 | // if (template && template[0] && template[0].__component) { 18 | // const componentName = template[0].__component; 19 | // return global.strapi.components[componentName]; 20 | // } 21 | 22 | // throw new PreviewError(400, "Template field is incompatible"); 23 | // }; 24 | 25 | /** 26 | * preview.js service 27 | * 28 | * @description: A set of functions similar to controller's actions to avoid code duplication. 29 | */ 30 | module.exports = { 31 | /** 32 | * Get if content type is previewable 33 | * 34 | * @param contentType - The content type to check 35 | * 36 | * @returns - Returns inf content type is previewable 37 | */ 38 | async isPreviewable(contentType: string) { 39 | const model = await global.strapi.query(contentType)?.model; 40 | 41 | if (model) { 42 | return model.pluginOptions?.['preview-content']?.previewable || model.options?.previewable; 43 | } 44 | throw new PreviewError(400, "Wrong contentType"); 45 | }, 46 | /** 47 | * Find a content type by id previewable 48 | * 49 | * @param - The content type to query 50 | * @param - The ID of the content to query 51 | * @param - The query string params from URL 52 | * 53 | * @returns Returns an object with the template name, content type and data; otherwise error 400. 54 | */ 55 | async findOne( 56 | contentType: string, 57 | id: string, 58 | query: Record 59 | ) { 60 | const service = global.strapi.services[contentType]; 61 | const model = global.strapi.models[contentType]; 62 | 63 | if (!service) { 64 | throw new PreviewError(400, "Wrong contentType"); 65 | } 66 | 67 | if (!model.options.previewable) { 68 | throw new PreviewError(400, "This content type is not previewable"); 69 | } 70 | 71 | let contentPreview: any; 72 | 73 | const contentPreviewPublished = await service.findOne({ 74 | ...query, 75 | id, 76 | }); 77 | 78 | if (contentPreviewPublished) { 79 | contentPreview = contentPreviewPublished; 80 | } 81 | 82 | const contentPreviewDraft = await service.findOne({ 83 | ...query, 84 | id, 85 | }); 86 | 87 | if (contentPreviewDraft) { 88 | contentPreview = contentPreviewDraft; 89 | } 90 | 91 | if (!contentPreview) { 92 | throw new PreviewError( 93 | 404, 94 | "Preview not found for given content type and Id" 95 | ); 96 | } 97 | 98 | const data = sanitizeEntity(contentPreview, { model }); 99 | // const templateComponent = getTemplateComponentFromTemplate(data.template); 100 | 101 | return { 102 | // templateName: templateComponent.options.templateName, 103 | contentType, 104 | data, 105 | }; 106 | }, 107 | /** 108 | * Get the preview url from a content type by id 109 | * 110 | * @param - The content type to query 111 | * @param - The content type id to query 112 | * @param - The query strings from URL 113 | * 114 | * @returns The preview URL parsed with `replacePreviewParams()` 115 | */ 116 | async getPreviewUrl( 117 | contentType: string, 118 | contentId: string, 119 | _query: Record 120 | ) { 121 | // @ts-ignore 122 | const contentTypeModel = strapi.models[contentType] 123 | const contentTypeConfig = contentTypeModel?.pluginOptions?.['preview-content']; 124 | 125 | const entity = await this.getSettings(); 126 | 127 | const previewUrl = contentTypeConfig?.url || entity.previewUrl || ""; 128 | const baseUrl = entity.baseUrl || ""; 129 | 130 | // Fetch data that needs to be put into the url (if enabled) 131 | let additionalValues = {} 132 | if (contentTypeConfig?.usesValuesInUrl) { 133 | // Fetch the data 134 | // @ts-ignore 135 | additionalValues = await strapi.query(contentType).findOne({ id: contentId }) 136 | } 137 | 138 | return this.replacePreviewParams(baseUrl, contentType, contentId, previewUrl, additionalValues); 139 | }, 140 | /** 141 | * Replace URL from string params 142 | * 143 | * @param - The root url of the project's frontend 144 | * @param - The content type to query 145 | * @param - The content type id to query 146 | * @param - The url string to replace 147 | * @param - Additional data of the specific content type that needs to be injected into the url 148 | * 149 | * @returns The replaced URL 150 | */ 151 | replacePreviewParams(baseUrl: string, contentType: string, contentId: string, url: string, additionalValues: object) { 152 | return _.template( 153 | url 154 | .replace(":baseUrl", baseUrl) 155 | .replace(":contentType", contentType) 156 | .replace(":id", contentId) 157 | )(additionalValues); 158 | }, 159 | /** 160 | * Get settings of the plugin 161 | */ 162 | async getSettings() { 163 | // @ts-ignore 164 | return strapi 165 | .store({ 166 | type: "plugin", 167 | name: "preview-content", 168 | key: "settings", 169 | }) 170 | .get(); 171 | }, 172 | /** 173 | * Update settings of the plugin 174 | */ 175 | async setSettings(value: any) { 176 | // @ts-ignore 177 | return strapi 178 | .store({ 179 | type: "plugin", 180 | name: "preview-content", 181 | key: "settings", 182 | }) 183 | .set({ value }); 184 | }, 185 | }; 186 | -------------------------------------------------------------------------------- /strapi-files/v3.6.x/README.md: -------------------------------------------------------------------------------- 1 | ## This files work for Strapi >=3.4.x 2 | -------------------------------------------------------------------------------- /strapi-files/v3.6.x/extensions/content-manager/admin/src/components/CustomTable/Row/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { toString } from "lodash"; 4 | import { useGlobalContext, request } from "strapi-helper-plugin"; 5 | import { IconLinks } from "@buffetjs/core"; 6 | import { Duplicate } from "@buffetjs/icons"; 7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 8 | import { useListView } from "../../../hooks"; 9 | import { getDisplayedValue } from "../../../utils"; 10 | import CustomInputCheckbox from "../../CustomInputCheckbox"; 11 | import ActionContainer from "./ActionContainer"; 12 | import Cell from "./Cell"; 13 | 14 | /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 15 | 16 | function Row({ 17 | canCreate, 18 | canDelete, 19 | canUpdate, 20 | isBulkable, 21 | row, 22 | headers, 23 | goTo, 24 | apiID, 25 | previewable, 26 | }) { 27 | const { entriesToDelete, onChangeBulk, onClickDelete } = useListView(); 28 | const { emitEvent } = useGlobalContext(); 29 | const emitEventRef = useRef(emitEvent); 30 | 31 | const memoizedDisplayedValue = useCallback( 32 | (name, type) => { 33 | return getDisplayedValue(type, row[name], name); 34 | }, 35 | [row] 36 | ); 37 | 38 | const links = [ 39 | previewable && { 40 | icon: , 41 | onClick: async (e) => { 42 | e.stopPropagation(); 43 | 44 | console.log({ apiID }); 45 | 46 | try { 47 | const data = await request( 48 | `/preview-content/preview-url/${apiID}/${row.id}`, 49 | { 50 | method: "GET", 51 | } 52 | ); 53 | 54 | if (data.url) { 55 | window.open(data.url, "_blank"); 56 | } else { 57 | strapi.notification.error("URL not found"); 58 | } 59 | } catch (_e) { 60 | strapi.notification.error("URL not found"); 61 | } 62 | }, 63 | }, 64 | { 65 | icon: canCreate ? : null, 66 | onClick: (e) => { 67 | e.stopPropagation(); 68 | goTo(`create/clone/${row.id}`); 69 | }, 70 | }, 71 | { 72 | icon: canUpdate ? : null, 73 | onClick: (e) => { 74 | e.stopPropagation(); 75 | emitEventRef.current("willDeleteEntryFromList"); 76 | goTo(row.id); 77 | }, 78 | }, 79 | { 80 | icon: canDelete ? : null, 81 | onClick: (e) => { 82 | e.stopPropagation(); 83 | emitEventRef.current("willDeleteEntryFromList"); 84 | onClickDelete(row.id); 85 | }, 86 | }, 87 | ].filter((icon) => icon); 88 | 89 | return ( 90 | <> 91 | {isBulkable && ( 92 | // eslint-disable-next-line jsx-a11y/click-events-have-key-events 93 | e.stopPropagation()}> 94 | toString(id) === toString(row.id)) 99 | .length > 0 100 | } 101 | /> 102 | 103 | )} 104 | {headers.map( 105 | ({ 106 | key, 107 | name, 108 | fieldSchema: { type, relationType }, 109 | cellFormatter, 110 | metadatas, 111 | queryInfos, 112 | }) => ( 113 | 114 | {cellFormatter ? ( 115 | cellFormatter(row) 116 | ) : ( 117 | 129 | )} 130 | 131 | ) 132 | )} 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | 140 | Row.propTypes = { 141 | canCreate: PropTypes.bool.isRequired, 142 | canDelete: PropTypes.bool.isRequired, 143 | canUpdate: PropTypes.bool.isRequired, 144 | headers: PropTypes.array.isRequired, 145 | isBulkable: PropTypes.bool.isRequired, 146 | row: PropTypes.object.isRequired, 147 | goTo: PropTypes.func.isRequired, 148 | apiID: PropTypes.string, 149 | previewable: PropTypes.bool.isRequired, 150 | }; 151 | 152 | export default memo(Row); 153 | -------------------------------------------------------------------------------- /strapi-files/v3.6.x/extensions/content-manager/admin/src/components/CustomTable/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useMemo, useState, useEffect } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useLocation, useHistory } from "react-router-dom"; 4 | import { FormattedMessage, useIntl } from "react-intl"; 5 | import { upperFirst, isEmpty } from "lodash"; 6 | import { 7 | LoadingIndicator, 8 | useGlobalContext, 9 | request, 10 | } from "strapi-helper-plugin"; 11 | import useListView from "../../hooks/useListView"; 12 | import { getTrad } from "../../utils"; 13 | import State from "../State"; 14 | import { 15 | LoadingContainer, 16 | LoadingWrapper, 17 | Table, 18 | TableEmpty, 19 | TableRow, 20 | } from "./styledComponents"; 21 | import ActionCollapse from "./ActionCollapse"; 22 | import Headers from "./Headers"; 23 | import Row from "./Row"; 24 | 25 | const CustomTable = ({ 26 | canCreate, 27 | canUpdate, 28 | canDelete, 29 | data, 30 | displayedHeaders, 31 | hasDraftAndPublish, 32 | isBulkable, 33 | showLoader, 34 | apiID, 35 | }) => { 36 | const { formatMessage } = useIntl(); 37 | const { entriesToDelete, label, filters, _q } = useListView(); 38 | const { emitEvent } = useGlobalContext(); 39 | const [previewable, setIsPreviewable] = useState(false); 40 | 41 | const { pathname } = useLocation(); 42 | const { push } = useHistory(); 43 | const headers = useMemo(() => { 44 | if (hasDraftAndPublish) { 45 | return [ 46 | ...displayedHeaders, 47 | { 48 | key: "__published_at_temp_key__", 49 | name: "published_at", 50 | fieldSchema: { 51 | type: "custom", 52 | }, 53 | metadatas: { 54 | label: formatMessage({ 55 | id: getTrad("containers.ListPage.table-headers.published_at"), 56 | }), 57 | searchable: false, 58 | sortable: true, 59 | }, 60 | cellFormatter: (cellData) => { 61 | const isPublished = !isEmpty(cellData.published_at); 62 | 63 | return ; 64 | }, 65 | }, 66 | ]; 67 | } 68 | 69 | return displayedHeaders; 70 | }, [formatMessage, hasDraftAndPublish, displayedHeaders]); 71 | 72 | const colSpanLength = 73 | isBulkable && canDelete ? headers.length + 2 : headers.length + 1; 74 | 75 | useEffect(() => { 76 | if (apiID) { 77 | request(`/preview-content/is-previewable/${apiID}`, { 78 | method: "GET", 79 | }).then(({ isPreviewable }) => { 80 | setIsPreviewable(isPreviewable); 81 | }); 82 | } 83 | }, [apiID]); 84 | 85 | const handleRowGoTo = (id) => { 86 | emitEvent("willEditEntryFromList"); 87 | push({ 88 | pathname: `${pathname}/${id}`, 89 | state: { from: pathname }, 90 | }); 91 | }; 92 | const handleEditGoTo = (id) => { 93 | emitEvent("willEditEntryFromButton"); 94 | push({ 95 | pathname: `${pathname}/${id}`, 96 | state: { from: pathname }, 97 | }); 98 | }; 99 | 100 | const values = { contentType: upperFirst(label), search: _q }; 101 | let tableEmptyMsgId = filters.length > 0 ? "withFilters" : "withoutFilter"; 102 | 103 | if (_q !== "") { 104 | tableEmptyMsgId = "withSearch"; 105 | } 106 | 107 | const content = 108 | data.length === 0 ? ( 109 | 110 | 111 | 115 | 116 | 117 | ) : ( 118 | data.map((row) => { 119 | return ( 120 | { 123 | e.preventDefault(); 124 | e.stopPropagation(); 125 | 126 | handleRowGoTo(row.id); 127 | }} 128 | > 129 | 140 | 141 | ); 142 | }) 143 | ); 144 | 145 | if (showLoader) { 146 | return ( 147 | <> 148 | 149 | 150 |
151 | 152 | 153 | 154 | 155 | 156 | 157 | ); 158 | } 159 | 160 | return ( 161 | 162 | 163 | 164 | {entriesToDelete.length > 0 && ( 165 | 166 | )} 167 | {content} 168 | 169 |
170 | ); 171 | }; 172 | 173 | CustomTable.propTypes = { 174 | canCreate: PropTypes.bool.isRequired, 175 | canDelete: PropTypes.bool.isRequired, 176 | canUpdate: PropTypes.bool.isRequired, 177 | data: PropTypes.array.isRequired, 178 | displayedHeaders: PropTypes.array.isRequired, 179 | hasDraftAndPublish: PropTypes.bool.isRequired, 180 | isBulkable: PropTypes.bool.isRequired, 181 | showLoader: PropTypes.bool.isRequired, 182 | apiID: PropTypes.string, 183 | }; 184 | 185 | export default memo(CustomTable); 186 | -------------------------------------------------------------------------------- /strapi-files/v3.6.x/extensions/content-manager/admin/src/containers/EditView/Header/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useMemo, useRef, useState } from "react"; 2 | import { useIntl } from "react-intl"; 3 | import { Header as PluginHeader } from "@buffetjs/custom"; 4 | import { get, isEqual, isEmpty, toString } from "lodash"; 5 | import PropTypes from "prop-types"; 6 | import isEqualFastCompare from "react-fast-compare"; 7 | import { Text } from "@buffetjs/core"; 8 | import { templateObject, ModalConfirm } from "strapi-helper-plugin"; 9 | import { usePreview } from "strapi-plugin-preview-content"; 10 | import { getTrad } from "../../../utils"; 11 | import { connect, getDraftRelations, select } from "./utils"; 12 | 13 | const primaryButtonObject = { 14 | color: "primary", 15 | type: "button", 16 | style: { 17 | minWidth: 150, 18 | fontWeight: 600, 19 | }, 20 | }; 21 | 22 | const Header = ({ 23 | allowedActions: { canUpdate, canCreate, canPublish }, 24 | componentLayouts, 25 | initialData, 26 | isCreatingEntry, 27 | isSingleType, 28 | hasDraftAndPublish, 29 | layout, 30 | modifiedData, 31 | onPublish, 32 | onUnpublish, 33 | status, 34 | }) => { 35 | const { previewHeaderActions } = usePreview(); 36 | const [showWarningUnpublish, setWarningUnpublish] = useState(false); 37 | const { formatMessage } = useIntl(); 38 | const formatMessageRef = useRef(formatMessage); 39 | const [draftRelationsCount, setDraftRelationsCount] = useState(0); 40 | const [showWarningDraftRelation, setShowWarningDraftRelation] = useState( 41 | false 42 | ); 43 | const [shouldUnpublish, setShouldUnpublish] = useState(false); 44 | const [shouldPublish, setShouldPublish] = useState(false); 45 | 46 | const currentContentTypeMainField = useMemo( 47 | () => get(layout, ["settings", "mainField"], "id"), 48 | [layout] 49 | ); 50 | 51 | const currentContentTypeName = useMemo(() => get(layout, ["info", "name"]), [ 52 | layout, 53 | ]); 54 | 55 | const didChangeData = useMemo(() => { 56 | return ( 57 | !isEqual(initialData, modifiedData) || 58 | (isCreatingEntry && !isEmpty(modifiedData)) 59 | ); 60 | }, [initialData, isCreatingEntry, modifiedData]); 61 | const apiID = useMemo(() => layout.apiID, [layout.apiID]); 62 | 63 | /* eslint-disable indent */ 64 | const entryHeaderTitle = isCreatingEntry 65 | ? formatMessage({ 66 | id: getTrad("containers.Edit.pluginHeader.title.new"), 67 | }) 68 | : templateObject({ mainField: currentContentTypeMainField }, initialData) 69 | .mainField; 70 | /* eslint-enable indent */ 71 | 72 | const headerTitle = useMemo(() => { 73 | const title = isSingleType ? currentContentTypeName : entryHeaderTitle; 74 | 75 | return title || currentContentTypeName; 76 | }, [currentContentTypeName, entryHeaderTitle, isSingleType]); 77 | 78 | const checkIfHasDraftRelations = useCallback(() => { 79 | const count = getDraftRelations(modifiedData, layout, componentLayouts); 80 | 81 | setDraftRelationsCount(count); 82 | 83 | return count > 0; 84 | }, [modifiedData, layout, componentLayouts]); 85 | 86 | const headerActions = useMemo(() => { 87 | let headerActions = []; 88 | 89 | if ((isCreatingEntry && canCreate) || (!isCreatingEntry && canUpdate)) { 90 | headerActions = [ 91 | { 92 | disabled: !didChangeData, 93 | color: "success", 94 | label: formatMessage({ 95 | id: getTrad("containers.Edit.submit"), 96 | }), 97 | isLoading: status === "submit-pending", 98 | type: "submit", 99 | style: { 100 | minWidth: 150, 101 | fontWeight: 600, 102 | }, 103 | }, 104 | ]; 105 | } 106 | 107 | if (hasDraftAndPublish && canPublish) { 108 | const isPublished = !isEmpty(initialData.published_at); 109 | const isLoading = isPublished 110 | ? status === "unpublish-pending" 111 | : status === "publish-pending"; 112 | const labelID = isPublished ? "app.utils.unpublish" : "app.utils.publish"; 113 | /* eslint-disable indent */ 114 | const onClick = isPublished 115 | ? () => setWarningUnpublish(true) 116 | : (e) => { 117 | if (!checkIfHasDraftRelations()) { 118 | onPublish(e); 119 | } else { 120 | setShowWarningDraftRelation(true); 121 | } 122 | }; 123 | /* eslint-enable indent */ 124 | 125 | const action = { 126 | ...primaryButtonObject, 127 | disabled: isCreatingEntry || didChangeData, 128 | isLoading, 129 | label: formatMessage({ id: labelID }), 130 | onClick, 131 | }; 132 | 133 | headerActions.unshift(action); 134 | } 135 | 136 | return [...previewHeaderActions, ...headerActions]; 137 | }, [ 138 | isCreatingEntry, 139 | canCreate, 140 | canUpdate, 141 | hasDraftAndPublish, 142 | canPublish, 143 | didChangeData, 144 | formatMessage, 145 | status, 146 | initialData, 147 | onPublish, 148 | checkIfHasDraftRelations, 149 | previewHeaderActions, 150 | ]); 151 | 152 | const headerProps = useMemo(() => { 153 | return { 154 | title: { 155 | label: toString(headerTitle), 156 | }, 157 | content: `${formatMessageRef.current({ 158 | id: getTrad("api.id"), 159 | })} : ${apiID}`, 160 | actions: headerActions, 161 | }; 162 | }, [headerActions, headerTitle, apiID]); 163 | 164 | const toggleWarningPublish = () => 165 | setWarningUnpublish((prevState) => !prevState); 166 | 167 | const toggleWarningDraftRelation = useCallback(() => { 168 | setShowWarningDraftRelation((prev) => !prev); 169 | }, []); 170 | 171 | const handleConfirmPublish = useCallback(() => { 172 | setShouldPublish(true); 173 | setShowWarningDraftRelation(false); 174 | }, []); 175 | 176 | const handleConfirmUnpublish = useCallback(() => { 177 | setShouldUnpublish(true); 178 | setWarningUnpublish(false); 179 | }, []); 180 | 181 | const handleCloseModalPublish = useCallback( 182 | (e) => { 183 | if (shouldPublish) { 184 | onPublish(e); 185 | } 186 | 187 | setShouldUnpublish(false); 188 | }, 189 | [onPublish, shouldPublish] 190 | ); 191 | 192 | const handleCloseModalUnpublish = useCallback( 193 | (e) => { 194 | if (shouldUnpublish) { 195 | onUnpublish(e); 196 | } 197 | 198 | setShouldUnpublish(false); 199 | }, 200 | [onUnpublish, shouldUnpublish] 201 | ); 202 | 203 | const contentIdSuffix = draftRelationsCount > 1 ? "plural" : "singular"; 204 | 205 | return ( 206 | <> 207 | 208 | {hasDraftAndPublish && ( 209 | <> 210 |
, 217 | }, 218 | }} 219 | type="xwarning" 220 | onConfirm={handleConfirmUnpublish} 221 | onClosed={handleCloseModalUnpublish} 222 | > 223 | 224 | {formatMessage({ 225 | id: getTrad("popUpWarning.warning.unpublish-question"), 226 | })} 227 | 228 |
229 | ( 247 | 248 | {chunks} 249 | 250 | ), 251 | br: () =>
, 252 | }, 253 | }} 254 | > 255 | 256 | {formatMessage({ 257 | id: getTrad("popUpWarning.warning.publish-question"), 258 | })} 259 | 260 |
261 | 262 | )} 263 | 264 | ); 265 | }; 266 | 267 | Header.propTypes = { 268 | allowedActions: PropTypes.shape({ 269 | canUpdate: PropTypes.bool.isRequired, 270 | canCreate: PropTypes.bool.isRequired, 271 | canPublish: PropTypes.bool.isRequired, 272 | }).isRequired, 273 | componentLayouts: PropTypes.object.isRequired, 274 | initialData: PropTypes.object.isRequired, 275 | isCreatingEntry: PropTypes.bool.isRequired, 276 | isSingleType: PropTypes.bool.isRequired, 277 | status: PropTypes.string.isRequired, 278 | layout: PropTypes.object.isRequired, 279 | hasDraftAndPublish: PropTypes.bool.isRequired, 280 | modifiedData: PropTypes.object.isRequired, 281 | onPublish: PropTypes.func.isRequired, 282 | onUnpublish: PropTypes.func.isRequired, 283 | }; 284 | 285 | const Memoized = memo(Header, isEqualFastCompare); 286 | 287 | export default connect(Memoized, select); 288 | -------------------------------------------------------------------------------- /strapi-files/v3.6.x/extensions/content-manager/admin/src/containers/EditView/Header/utils/connect.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { PreviewProvider } from "strapi-plugin-preview-content"; 3 | 4 | 5 | import { useContentManagerEditViewDataManager } from 'strapi-helper-plugin'; 6 | 7 | 8 | function connect(WrappedComponent, select) { 9 | return function (props) { 10 | // eslint-disable-next-line react/prop-types 11 | const selectors = select(); 12 | console.log(useContentManagerEditViewDataManager()); 13 | const { slug } = useContentManagerEditViewDataManager(); 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | }; 21 | } 22 | 23 | export default connect; 24 | -------------------------------------------------------------------------------- /strapi-files/v3.6.x/extensions/content-manager/admin/src/containers/ListView/index.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { connect } from 'react-redux'; 4 | import { bindActionCreators, compose } from 'redux'; 5 | import { get, isEmpty } from 'lodash'; 6 | import { FormattedMessage, useIntl } from 'react-intl'; 7 | import { useHistory, useLocation } from 'react-router-dom'; 8 | import { Header } from '@buffetjs/custom'; 9 | import { Flex, Padded } from '@buffetjs/core'; 10 | import isEqual from 'react-fast-compare'; 11 | import { stringify } from 'qs'; 12 | import { 13 | CheckPermissions, 14 | InjectionZone, 15 | InjectionZoneList, 16 | PopUpWarning, 17 | useGlobalContext, 18 | useQueryParams, 19 | useUser, 20 | request, 21 | } from 'strapi-helper-plugin'; 22 | import pluginId from '../../pluginId'; 23 | import pluginPermissions from '../../permissions'; 24 | import { formatFiltersFromQuery, getRequestUrl, getTrad } from '../../utils'; 25 | import Container from '../../components/Container'; 26 | import CustomTable from '../../components/CustomTable'; 27 | import FilterPicker from '../../components/FilterPicker'; 28 | import Search from '../../components/Search'; 29 | import ListViewProvider from '../ListViewProvider'; 30 | import { AddFilterCta, FilterIcon, Wrapper } from './components'; 31 | import FieldPicker from './FieldPicker'; 32 | import Filter from './Filter'; 33 | import Footer from './Footer'; 34 | import { 35 | getData, 36 | getDataSucceeded, 37 | onChangeBulk, 38 | onChangeBulkSelectall, 39 | onDeleteDataError, 40 | onDeleteDataSucceeded, 41 | onDeleteSeveralDataSucceeded, 42 | setModalLoadingState, 43 | toggleModalDelete, 44 | toggleModalDeleteAll, 45 | setLayout, 46 | onChangeListHeaders, 47 | onResetListHeaders, 48 | } from './actions'; 49 | import makeSelectListView from './selectors'; 50 | import { getAllAllowedHeaders, getFirstSortableHeader, buildQueryString } from './utils'; 51 | 52 | /* eslint-disable react/no-array-index-key */ 53 | function ListView({ 54 | canCreate, 55 | canDelete, 56 | canRead, 57 | canUpdate, 58 | didDeleteData, 59 | entriesToDelete, 60 | onChangeBulk, 61 | onChangeBulkSelectall, 62 | onDeleteDataError, 63 | onDeleteDataSucceeded, 64 | onDeleteSeveralDataSucceeded, 65 | setModalLoadingState, 66 | showWarningDelete, 67 | showModalConfirmButtonLoading, 68 | showWarningDeleteAll, 69 | toggleModalDelete, 70 | toggleModalDeleteAll, 71 | data, 72 | displayedHeaders, 73 | getData, 74 | getDataSucceeded, 75 | isLoading, 76 | layout, 77 | onChangeListHeaders, 78 | onResetListHeaders, 79 | pagination: { total }, 80 | slug, 81 | }) { 82 | const { 83 | contentType: { 84 | attributes, 85 | metadatas, 86 | settings: { bulkable: isBulkable, filterable: isFilterable, searchable: isSearchable }, 87 | }, 88 | } = layout; 89 | 90 | const { emitEvent } = useGlobalContext(); 91 | const { fetchUserPermissions } = useUser(); 92 | const emitEventRef = useRef(emitEvent); 93 | const fetchPermissionsRef = useRef(fetchUserPermissions); 94 | 95 | const [{ query }, setQuery] = useQueryParams(); 96 | const params = buildQueryString(query); 97 | 98 | const { pathname } = useLocation(); 99 | const { push } = useHistory(); 100 | const { formatMessage } = useIntl(); 101 | 102 | const [isFilterPickerOpen, setFilterPickerState] = useState(false); 103 | const [idToDelete, setIdToDelete] = useState(null); 104 | const contentType = layout.contentType; 105 | const hasDraftAndPublish = get(contentType, 'options.draftAndPublish', false); 106 | const allAllowedHeaders = useMemo(() => getAllAllowedHeaders(attributes), [attributes]); 107 | 108 | const filters = useMemo(() => { 109 | return formatFiltersFromQuery(query); 110 | }, [query]); 111 | 112 | const _sort = query._sort; 113 | const _q = query._q || ''; 114 | 115 | const label = contentType.info.label; 116 | 117 | const firstSortableHeader = useMemo(() => getFirstSortableHeader(displayedHeaders), [ 118 | displayedHeaders, 119 | ]); 120 | 121 | useEffect(() => { 122 | setFilterPickerState(false); 123 | }, []); 124 | 125 | // Using a ref to avoid requests being fired multiple times on slug on change 126 | // We need it because the hook as mulitple dependencies so it may run before the permissions have checked 127 | const requestUrlRef = useRef(''); 128 | 129 | const fetchData = useCallback( 130 | async (endPoint, abortSignal = false) => { 131 | getData(); 132 | const signal = abortSignal || new AbortController().signal; 133 | 134 | try { 135 | const { results, pagination } = await request(endPoint, { method: 'GET', signal }); 136 | 137 | getDataSucceeded(pagination, results); 138 | } catch (err) { 139 | const resStatus = get(err, 'response.status', null); 140 | console.log(err); 141 | 142 | if (resStatus === 403) { 143 | await fetchPermissionsRef.current(); 144 | 145 | strapi.notification.info(getTrad('permissions.not-allowed.update')); 146 | 147 | push('/'); 148 | 149 | return; 150 | } 151 | 152 | if (err.name !== 'AbortError') { 153 | console.error(err); 154 | strapi.notification.error(getTrad('error.model.fetch')); 155 | } 156 | } 157 | }, 158 | [getData, getDataSucceeded, push] 159 | ); 160 | 161 | const handleChangeListLabels = useCallback( 162 | ({ name, value }) => { 163 | // Display a notification if trying to remove the last displayed field 164 | 165 | if (value && displayedHeaders.length === 1) { 166 | strapi.notification.toggle({ 167 | type: 'warning', 168 | message: { id: 'content-manager.notification.error.displayedFields' }, 169 | }); 170 | } else { 171 | emitEventRef.current('didChangeDisplayedFields'); 172 | 173 | onChangeListHeaders({ name, value }); 174 | } 175 | }, 176 | [displayedHeaders, onChangeListHeaders] 177 | ); 178 | 179 | const handleConfirmDeleteAllData = useCallback(async () => { 180 | try { 181 | setModalLoadingState(); 182 | 183 | await request(getRequestUrl(`collection-types/${slug}/actions/bulkDelete`), { 184 | method: 'POST', 185 | body: { ids: entriesToDelete }, 186 | }); 187 | 188 | onDeleteSeveralDataSucceeded(); 189 | emitEventRef.current('didBulkDeleteEntries'); 190 | } catch (err) { 191 | strapi.notification.error(`${pluginId}.error.record.delete`); 192 | } 193 | }, [entriesToDelete, onDeleteSeveralDataSucceeded, slug, setModalLoadingState]); 194 | 195 | const handleConfirmDeleteData = useCallback(async () => { 196 | try { 197 | let trackerProperty = {}; 198 | 199 | if (hasDraftAndPublish) { 200 | const dataToDelete = data.find(obj => obj.id.toString() === idToDelete.toString()); 201 | const isDraftEntry = isEmpty(dataToDelete.published_at); 202 | const status = isDraftEntry ? 'draft' : 'published'; 203 | 204 | trackerProperty = { status }; 205 | } 206 | 207 | emitEventRef.current('willDeleteEntry', trackerProperty); 208 | setModalLoadingState(); 209 | 210 | await request(getRequestUrl(`collection-types/${slug}/${idToDelete}`), { 211 | method: 'DELETE', 212 | }); 213 | 214 | strapi.notification.toggle({ 215 | type: 'success', 216 | message: { id: `${pluginId}.success.record.delete` }, 217 | }); 218 | 219 | // Close the modal and refetch data 220 | onDeleteDataSucceeded(); 221 | emitEventRef.current('didDeleteEntry', trackerProperty); 222 | } catch (err) { 223 | const errorMessage = get( 224 | err, 225 | 'response.payload.message', 226 | formatMessage({ id: `${pluginId}.error.record.delete` }) 227 | ); 228 | 229 | strapi.notification.toggle({ 230 | type: 'warning', 231 | message: errorMessage, 232 | }); 233 | // Close the modal 234 | onDeleteDataError(); 235 | } 236 | }, [ 237 | hasDraftAndPublish, 238 | setModalLoadingState, 239 | slug, 240 | idToDelete, 241 | onDeleteDataSucceeded, 242 | data, 243 | formatMessage, 244 | onDeleteDataError, 245 | ]); 246 | 247 | useEffect(() => { 248 | const abortController = new AbortController(); 249 | const { signal } = abortController; 250 | 251 | const shouldSendRequest = canRead; 252 | const requestUrl = `/${pluginId}/collection-types/${slug}${params}`; 253 | 254 | if (shouldSendRequest && requestUrl.includes(requestUrlRef.current)) { 255 | fetchData(requestUrl, signal); 256 | } 257 | 258 | return () => { 259 | requestUrlRef.current = slug; 260 | abortController.abort(); 261 | }; 262 | }, [canRead, getData, slug, params, getDataSucceeded, fetchData]); 263 | 264 | const handleClickDelete = id => { 265 | setIdToDelete(id); 266 | toggleModalDelete(); 267 | }; 268 | 269 | const handleModalClose = useCallback(() => { 270 | if (didDeleteData) { 271 | const requestUrl = `/${pluginId}/collection-types/${slug}${params}`; 272 | 273 | fetchData(requestUrl); 274 | } 275 | }, [fetchData, didDeleteData, slug, params]); 276 | 277 | const toggleFilterPickerState = useCallback(() => { 278 | setFilterPickerState(prevState => { 279 | if (!prevState) { 280 | emitEventRef.current('willFilterEntries'); 281 | } 282 | 283 | return !prevState; 284 | }); 285 | }, []); 286 | 287 | const headerAction = useMemo(() => { 288 | if (!canCreate) { 289 | return []; 290 | } 291 | 292 | return [ 293 | { 294 | label: formatMessage( 295 | { 296 | id: 'content-manager.containers.List.addAnEntry', 297 | }, 298 | { 299 | entity: label || 'Content Manager', 300 | } 301 | ), 302 | onClick: () => { 303 | const trackerProperty = hasDraftAndPublish ? { status: 'draft' } : {}; 304 | 305 | emitEventRef.current('willCreateEntry', trackerProperty); 306 | push({ 307 | pathname: `${pathname}/create`, 308 | search: query.plugins ? stringify({ plugins: query.plugins }, { encode: false }) : '', 309 | }); 310 | }, 311 | color: 'primary', 312 | type: 'button', 313 | icon: true, 314 | style: { 315 | paddingLeft: 15, 316 | paddingRight: 15, 317 | fontWeight: 600, 318 | }, 319 | }, 320 | ]; 321 | }, [label, pathname, canCreate, formatMessage, hasDraftAndPublish, push, query]); 322 | 323 | const headerProps = useMemo(() => { 324 | /* eslint-disable indent */ 325 | return { 326 | title: { 327 | label: label || 'Content Manager', 328 | }, 329 | content: canRead 330 | ? formatMessage( 331 | { 332 | id: 333 | total > 1 334 | ? `${pluginId}.containers.List.pluginHeaderDescription` 335 | : `${pluginId}.containers.List.pluginHeaderDescription.singular`, 336 | }, 337 | { label: total } 338 | ) 339 | : null, 340 | actions: headerAction, 341 | }; 342 | }, [total, headerAction, label, canRead, formatMessage]); 343 | 344 | const handleToggleModalDeleteAll = e => { 345 | emitEventRef.current('willBulkDeleteEntries'); 346 | toggleModalDeleteAll(e); 347 | }; 348 | 349 | return ( 350 | <> 351 | 366 | 376 | 377 | {!isFilterPickerOpen &&
} 378 | {isSearchable && canRead && ( 379 | 380 | )} 381 | 382 | {!canRead && ( 383 | 384 | 385 | 386 | 387 | 388 | )} 389 | 390 | {canRead && ( 391 | 392 |
393 |
394 |
395 | {isFilterable && ( 396 | <> 397 | 398 | 399 | 400 | 401 | {filters.map(({ filter: filterName, name, value }, key) => ( 402 | 415 | ))} 416 | 417 | )} 418 |
419 |
420 | 421 |
422 | 423 | 424 | 425 | 426 | 427 | 428 | 435 | 436 | 437 |
438 |
439 |
440 |
441 | 453 |
454 |
455 |
456 |
457 | )} 458 | 459 | 470 | 471 | 472 | 1 ? '.all' : '' 478 | }` 479 | ), 480 | }} 481 | popUpWarningType="danger" 482 | onConfirm={handleConfirmDeleteAllData} 483 | onClosed={handleModalClose} 484 | isConfirmButtonLoading={showModalConfirmButtonLoading} 485 | > 486 | 487 | 488 | 489 | 490 | ); 491 | } 492 | 493 | ListView.defaultProps = { 494 | permissions: [], 495 | }; 496 | 497 | ListView.propTypes = { 498 | canCreate: PropTypes.bool.isRequired, 499 | canDelete: PropTypes.bool.isRequired, 500 | canRead: PropTypes.bool.isRequired, 501 | canUpdate: PropTypes.bool.isRequired, 502 | displayedHeaders: PropTypes.array.isRequired, 503 | data: PropTypes.array.isRequired, 504 | didDeleteData: PropTypes.bool.isRequired, 505 | entriesToDelete: PropTypes.array.isRequired, 506 | layout: PropTypes.exact({ 507 | components: PropTypes.object.isRequired, 508 | contentType: PropTypes.shape({ 509 | attributes: PropTypes.object.isRequired, 510 | metadatas: PropTypes.object.isRequired, 511 | info: PropTypes.shape({ label: PropTypes.string.isRequired }).isRequired, 512 | layouts: PropTypes.shape({ 513 | list: PropTypes.array.isRequired, 514 | editRelations: PropTypes.array, 515 | }).isRequired, 516 | options: PropTypes.object.isRequired, 517 | settings: PropTypes.object.isRequired, 518 | }).isRequired, 519 | }).isRequired, 520 | isLoading: PropTypes.bool.isRequired, 521 | getData: PropTypes.func.isRequired, 522 | getDataSucceeded: PropTypes.func.isRequired, 523 | onChangeBulk: PropTypes.func.isRequired, 524 | onChangeBulkSelectall: PropTypes.func.isRequired, 525 | onChangeListHeaders: PropTypes.func.isRequired, 526 | onDeleteDataError: PropTypes.func.isRequired, 527 | onDeleteDataSucceeded: PropTypes.func.isRequired, 528 | onDeleteSeveralDataSucceeded: PropTypes.func.isRequired, 529 | onResetListHeaders: PropTypes.func.isRequired, 530 | pagination: PropTypes.shape({ total: PropTypes.number.isRequired }).isRequired, 531 | setModalLoadingState: PropTypes.func.isRequired, 532 | showModalConfirmButtonLoading: PropTypes.bool.isRequired, 533 | showWarningDelete: PropTypes.bool.isRequired, 534 | showWarningDeleteAll: PropTypes.bool.isRequired, 535 | slug: PropTypes.string.isRequired, 536 | toggleModalDelete: PropTypes.func.isRequired, 537 | toggleModalDeleteAll: PropTypes.func.isRequired, 538 | setLayout: PropTypes.func.isRequired, 539 | permissions: PropTypes.arrayOf( 540 | PropTypes.shape({ 541 | action: PropTypes.string.isRequired, 542 | subject: PropTypes.string.isRequired, 543 | properties: PropTypes.object, 544 | conditions: PropTypes.arrayOf(PropTypes.string), 545 | }) 546 | ), 547 | }; 548 | 549 | const mapStateToProps = makeSelectListView(); 550 | 551 | export function mapDispatchToProps(dispatch) { 552 | return bindActionCreators( 553 | { 554 | getData, 555 | getDataSucceeded, 556 | onChangeBulk, 557 | onChangeBulkSelectall, 558 | onChangeListHeaders, 559 | onDeleteDataError, 560 | onDeleteDataSucceeded, 561 | onDeleteSeveralDataSucceeded, 562 | onResetListHeaders, 563 | setModalLoadingState, 564 | toggleModalDelete, 565 | toggleModalDeleteAll, 566 | setLayout, 567 | }, 568 | dispatch 569 | ); 570 | } 571 | const withConnect = connect(mapStateToProps, mapDispatchToProps); 572 | 573 | export default compose(withConnect)(memo(ListView, isEqual)); 574 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": ".", 4 | "lib": ["esnext", "dom"], 5 | "target": "es5", 6 | "module": "commonjs", 7 | "jsx": "react", 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "removeComments": false, 15 | "sourceMap": true, 16 | "declaration": true, 17 | "declarationMap": true, 18 | "downlevelIteration": true, 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "resolveJsonModule": true 22 | }, 23 | "include": ["external-types/*", "src/**/*", "src/**/*.json"], 24 | "exclude": ["**/node_modules/**/*", "**/dist/**/*"] 25 | } 26 | --------------------------------------------------------------------------------