Skip to content

Commit 7965ff8

Browse files
committed
V14 Integrations (Semrush)
- Add functionalities for Semrush
1 parent 7b43184 commit 7965ff8

30 files changed

+1259
-165
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using Asp.Versioning;
2+
using Microsoft.AspNetCore.Hosting;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.Extensions.Options;
6+
using Umbraco.Cms.Core.Services;
7+
using Umbraco.Cms.Integrations.SEO.Semrush.Configuration;
8+
using Umbraco.Cms.Integrations.SEO.Semrush.Models.Dtos;
9+
using Umbraco.Cms.Integrations.SEO.Semrush.Services;
10+
11+
namespace Umbraco.Cms.Integrations.SEO.Semrush.Api.Management.Controllers
12+
{
13+
[ApiVersion("1.0")]
14+
[ApiExplorerSettings(GroupName = Constants.ManagementApi.SemrushGroupName)]
15+
public class GetCurrentContentPropertiesController : SemrushControllerBase
16+
{
17+
private readonly IContentService _contentService;
18+
public GetCurrentContentPropertiesController(IOptions<SemrushSettings> options,
19+
IWebHostEnvironment webHostEnvironment,
20+
ISemrushTokenService semrushTokenService,
21+
ICacheHelper cacheHelper,
22+
TokenBuilder tokenBuilder,
23+
SemrushComposer.AuthorizationImplementationFactory authorizationImplementationFactory,
24+
IContentService contentService) : base(options, webHostEnvironment, semrushTokenService, cacheHelper, tokenBuilder, authorizationImplementationFactory)
25+
{
26+
_contentService = contentService;
27+
}
28+
29+
[HttpGet("content-properties")]
30+
[ProducesResponseType(typeof(List<ContentPropertyDto>), StatusCodes.Status200OK)]
31+
public async Task<IActionResult> GetCurrentContentProperties(string contentId)
32+
{
33+
var propertyList = new List<ContentPropertyDto>();
34+
35+
var content = _contentService.GetById(Guid.Parse(contentId));
36+
if (content != null)
37+
{
38+
propertyList = content.Properties.Select(s => new ContentPropertyDto { PropertyName = s.PropertyType.Name, PropertyValue = s.Values.ToList()[0].PublishedValue.ToString() }).ToList();
39+
}
40+
41+
return Ok(propertyList);
42+
}
43+
}
44+
}

src/Umbraco.Cms.Integrations.SEO.Semrush/Api/Management/Controllers/GetDataSourcesController.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
using Microsoft.AspNetCore.Http;
44
using Microsoft.AspNetCore.Mvc;
55
using Microsoft.Extensions.Options;
6-
using Newtonsoft.Json;
76
using System;
87
using System.Collections.Generic;
98
using System.Linq;
109
using System.Text;
10+
using System.Text.Json;
1111
using System.Threading.Tasks;
1212
using Umbraco.Cms.Integrations.SEO.Semrush.Configuration;
1313
using Umbraco.Cms.Integrations.SEO.Semrush.Models.Dtos;
@@ -27,13 +27,13 @@ public GetDataSourcesController(IOptions<SemrushSettings> options, IWebHostEnvir
2727
[ProducesResponseType(typeof(DataSourceDto), StatusCodes.Status200OK)]
2828
public IActionResult GetDataSources()
2929
{
30-
string semrushDataSourcesPath = $"{_webHostEnvironment.ContentRootPath}/App_Plugins/UmbracoCms.Integrations/SEO/Semrush/semrushDataSources.json";
30+
//string semrushDataSourcesPath = $"{_webHostEnvironment.ContentRootPath}/App_Plugins/UmbracoCms.Integrations/SEO/Semrush/semrushDataSources.json";
31+
string semrushDataSourcesPath = "semrushDataSources.json";
3132

3233
_lock.EnterReadLock();
33-
3434
try
3535
{
36-
if (!System.IO.File.Exists(semrushDataSourcesPath))
36+
if (!System.IO.File.Exists(Path.Combine(Directory.GetCurrentDirectory(), semrushDataSourcesPath)))
3737
{
3838
var fs = System.IO.File.Create(semrushDataSourcesPath);
3939
fs.Close();
@@ -44,7 +44,7 @@ public IActionResult GetDataSources()
4444
var content = System.IO.File.ReadAllText(semrushDataSourcesPath);
4545
var dataSourceDto = new DataSourceDto
4646
{
47-
Items = JsonConvert.DeserializeObject<List<DataSourceItemDto>>(content).Select(p =>
47+
Items = JsonSerializer.Deserialize<List<DataSourceItemDto>>(content).Select(p =>
4848
new DataSourceItemDto
4949
{
5050
Code = p.Code,

src/Umbraco.Cms.Integrations.SEO.Semrush/Api/Management/Controllers/GetRelatedPhrasesController.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
using System.Text;
1010
using System.Text.Json;
1111
using System.Threading.Tasks;
12+
using Umbraco.Cms.Core.Services;
1213
using Umbraco.Cms.Integrations.SEO.Semrush.Configuration;
1314
using Umbraco.Cms.Integrations.SEO.Semrush.Models.Dtos;
1415
using Umbraco.Cms.Integrations.SEO.Semrush.Services;
16+
using static Umbraco.Cms.Core.Constants.HttpContext;
1517

1618
namespace Umbraco.Cms.Integrations.SEO.Semrush.Api.Management.Controllers
1719
{
@@ -31,7 +33,7 @@ public async Task<IActionResult> GetRelatedPhrases(string phrase, int pageNumber
3133

3234
if (_cacheHelper.TryGetCachedItem<RelatedPhrasesDto>(cacheKey, out var relatedPhrasesDto) && relatedPhrasesDto.Data != null)
3335
{
34-
relatedPhrasesDto.TotalPages = relatedPhrasesDto.Data.Rows.Count / Constants.DefaultPageSize;
36+
relatedPhrasesDto.TotalPages = (int)Math.Ceiling((double)relatedPhrasesDto.Data.Rows.Count / Constants.DefaultPageSize);
3537
relatedPhrasesDto.Data.Rows = relatedPhrasesDto.Data.Rows
3638
.Skip((pageNumber - 1) * Constants.DefaultPageSize)
3739
.Take(Constants.DefaultPageSize)
@@ -47,21 +49,25 @@ public async Task<IActionResult> GetRelatedPhrases(string phrase, int pageNumber
4749

4850
if (response.IsSuccessStatusCode)
4951
{
50-
var responseContent = await response.Content.ReadAsStringAsync();
52+
//var responseContent = await response.Content.ReadAsStringAsync();
53+
using (StreamReader r = new StreamReader("temp.json"))
54+
{
55+
var responseContent = r.ReadToEnd();
5156

52-
var relatedPhrasesDeserialized = JsonSerializer.Deserialize<RelatedPhrasesDto>(responseContent);
57+
var relatedPhrasesDeserialized = JsonSerializer.Deserialize<RelatedPhrasesDto>(responseContent);
5358

54-
if (!relatedPhrasesDeserialized.IsSuccessful) return Ok(relatedPhrasesDeserialized);
59+
if (!relatedPhrasesDeserialized.IsSuccessful) return Ok(relatedPhrasesDeserialized);
5560

56-
_cacheHelper.AddCachedItem(cacheKey, responseContent);
61+
_cacheHelper.AddCachedItem(cacheKey, responseContent);
5762

58-
relatedPhrasesDeserialized.TotalPages = relatedPhrasesDeserialized.Data.Rows.Count / Constants.DefaultPageSize;
59-
relatedPhrasesDeserialized.Data.Rows = relatedPhrasesDeserialized.Data.Rows
60-
.Skip((pageNumber - 1) * Constants.DefaultPageSize)
61-
.Take(Constants.DefaultPageSize)
62-
.ToList();
63+
relatedPhrasesDeserialized.TotalPages = (int)Math.Ceiling((double)relatedPhrasesDeserialized.Data.Rows.Count / Constants.DefaultPageSize);
64+
relatedPhrasesDeserialized.Data.Rows = relatedPhrasesDeserialized.Data.Rows
65+
.Skip((pageNumber - 1) * Constants.DefaultPageSize)
66+
.Take(Constants.DefaultPageSize)
67+
.ToList();
6368

64-
return Ok(relatedPhrasesDeserialized);
69+
return Ok(relatedPhrasesDeserialized);
70+
}
6571
}
6672

6773
return Ok(relatedPhrasesDto);

src/Umbraco.Cms.Integrations.SEO.Semrush/Client/generated/services.gen.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import type { CancelablePromise } from './core/CancelablePromise';
44
import { OpenAPI } from './core/OpenAPI';
55
import { request as __request } from './core/request';
6-
import type { GetTokenDetailsResponse, GetAccessTokenData, GetAccessTokenResponse, RefreshAccessTokenResponse, RevokeTokenResponse, ValidateTokenResponse, OauthData, OauthResponse, GetAuthorizationUrlResponse, GetColumnsResponse, GetDataSourcesResponse, PingResponse, GetRelatedPhrasesData, GetRelatedPhrasesResponse } from './types.gen';
6+
import type { GetTokenDetailsResponse, GetAccessTokenData, GetAccessTokenResponse, RefreshAccessTokenResponse, RevokeTokenResponse, ValidateTokenResponse, OauthData, OauthResponse, GetAuthorizationUrlResponse, GetColumnsResponse, GetCurrentContentPropertiesData, GetCurrentContentPropertiesResponse, GetDataSourcesResponse, PingResponse, GetRelatedPhrasesData, GetRelatedPhrasesResponse } from './types.gen';
77

88
export class AccessTokenService {
99
/**
@@ -107,6 +107,22 @@ export class SemrushService {
107107
});
108108
}
109109

110+
/**
111+
* @param data The data for the request.
112+
* @param data.contentId
113+
* @returns unknown OK
114+
* @throws ApiError
115+
*/
116+
public static getCurrentContentProperties(data: GetCurrentContentPropertiesData = {}): CancelablePromise<GetCurrentContentPropertiesResponse> {
117+
return __request(OpenAPI, {
118+
method: 'GET',
119+
url: '/umbraco/semrush/management/api/v1/content-properties',
120+
query: {
121+
contentId: data.contentId
122+
}
123+
});
124+
}
125+
110126
/**
111127
* @returns unknown OK
112128
* @throws ApiError

src/Umbraco.Cms.Integrations.SEO.Semrush/Client/generated/types.gen.ts

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ export type ColumnDtoModel = {
1616
description: string;
1717
};
1818

19+
export type ContentPropertyDtoModel = {
20+
propertyName: string;
21+
propertyValue: string;
22+
};
23+
1924
export type ContentResult = {
2025
content?: string | null;
2126
contentType?: string | null;
@@ -55,16 +60,16 @@ export type RelatedPhrasesDataDtoModel = {
5560
export type RelatedPhrasesDtoModel = {
5661
readonly isSuccessful: boolean;
5762
error: string;
58-
status: string;
63+
status: number;
5964
data: RelatedPhrasesDataDtoModel;
6065
totalPages: number;
6166
};
6267

6368
export type TokenDtoModel = {
64-
accessToken: string;
65-
tokenType: string;
66-
expiresIn: number;
67-
refreshToken: string;
69+
access_token: string;
70+
token_type: string;
71+
expires_in: number;
72+
refresh_token: string;
6873
readonly isAccessTokenAvailable: boolean;
6974
};
7075

@@ -92,6 +97,12 @@ export type GetAuthorizationUrlResponse = string;
9297

9398
export type GetColumnsResponse = Array<(ColumnDtoModel)>;
9499

100+
export type GetCurrentContentPropertiesData = {
101+
contentId?: string;
102+
};
103+
104+
export type GetCurrentContentPropertiesResponse = Array<(ContentPropertyDtoModel)>;
105+
95106
export type GetDataSourcesResponse = DataSourceDtoModel;
96107

97108
export type PingResponse = string;
@@ -188,6 +199,17 @@ export type $OpenApiTs = {
188199
};
189200
};
190201
};
202+
'/umbraco/semrush/management/api/v1/content-properties': {
203+
get: {
204+
req: GetCurrentContentPropertiesData;
205+
res: {
206+
/**
207+
* OK
208+
*/
209+
200: Array<(ContentPropertyDtoModel)>;
210+
};
211+
};
212+
};
191213
'/umbraco/semrush/management/api/v1/datasources': {
192214
get: {
193215
res: {

src/Umbraco.Cms.Integrations.SEO.Semrush/Client/src/context/semrush.context.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@ import { UmbControllerBase } from "@umbraco-cms/backoffice/class-api";
22
import { UmbContextToken } from "@umbraco-cms/backoffice/context-api";
33
import type { UmbControllerHost } from "@umbraco-cms/backoffice/controller-api";
44
import { SemrushRepository } from "../repository/semrush.repository";
5+
import { UmbObjectState } from "@umbraco-cms/backoffice/observable-api";
6+
import { AuthorizationResponseDtoModel } from "@umbraco-integrations/semrush/generated";
57

68
export class SemrushContext extends UmbControllerBase{
79
#repository: SemrushRepository;
10+
#settingsModel = new UmbObjectState<AuthorizationResponseDtoModel | undefined>(undefined);
11+
settingsModel = this.#settingsModel.asObservable();
812

913
constructor(host: UmbControllerHost){
1014
super(host);
@@ -60,6 +64,10 @@ export class SemrushContext extends UmbControllerBase{
6064
async ping(){
6165
return await this.#repository.ping();
6266
}
67+
68+
async getCurrentContentProperties(contentId: string){
69+
return await this.#repository.getCurrentContentProperties(contentId);
70+
}
6371
}
6472

6573
export default SemrushContext;

src/Umbraco.Cms.Integrations.SEO.Semrush/Client/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { UMB_AUTH_CONTEXT } from "@umbraco-cms/backoffice/auth";
33
import { OpenAPI } from "@umbraco-integrations/semrush/generated";
44
import { manifest as semrushContext } from "./context/manifest";
55
import { manifests as workspaceManifest } from "./workspace/manifests";
6+
import { manifest as modalManifest } from "./modal/manifest";
67

78
export const onInit: UmbEntryPointOnInit = (host, extensionRegistry) => {
89
extensionRegistry.registerMany([
910
semrushContext,
11+
modalManifest,
1012
...workspaceManifest
1113
]);
1214

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { ManifestModal } from "@umbraco-cms/backoffice/extension-registry";
2+
3+
export const manifest: ManifestModal = {
4+
type: "modal",
5+
alias: "Semrush.Modal",
6+
name: "Semrush Modal",
7+
js: () => import("./semrush-modal.element")
8+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { UmbModalBaseElement } from "@umbraco-cms/backoffice/modal";
2+
import { css, html } from "lit";
3+
import { customElement, state } from "lit/decorators.js";
4+
import { SemrushModalData, SemrushModalValue } from "./semrush-modal.token";
5+
import { when } from "lit/directives/when.js";
6+
import { SEMRUSH_CONTEXT_TOKEN } from "../context/semrush.context";
7+
8+
const elementName = "shopify-products-modal";
9+
10+
@customElement(elementName)
11+
export default class ShopifyProductsModalElement extends UmbModalBaseElement<SemrushModalData, SemrushModalValue>{
12+
#semrushContext!: typeof SEMRUSH_CONTEXT_TOKEN.TYPE;
13+
14+
@state()
15+
private token: string = "";
16+
17+
@state()
18+
private isTokenAvailable: boolean = false;
19+
20+
constructor() {
21+
super();
22+
this.consumeContext(SEMRUSH_CONTEXT_TOKEN, (context) => {
23+
if (!context) return;
24+
this.#semrushContext = context;
25+
});
26+
}
27+
28+
async connectedCallback() {
29+
super.connectedCallback();
30+
31+
await this.#getTokenDetail();
32+
}
33+
34+
private isAuthorized(){
35+
return this.data?.authResponse?.isAuthorized;
36+
}
37+
38+
async #getTokenDetail(){
39+
var result = await this.#semrushContext.getTokenDetails();
40+
if (!result) return;
41+
42+
this.token = result.data?.access_token!;
43+
this.isTokenAvailable = result.data?.isAccessTokenAvailable!;
44+
}
45+
46+
async _revoke(){
47+
const result = await this.#semrushContext.revokeToken();
48+
if (!result) return;
49+
50+
this.value = {
51+
authResponse:{
52+
isAuthorized: false,
53+
isValid: false,
54+
isFreeAccount: false
55+
}
56+
}
57+
58+
this.requestUpdate();
59+
this.dispatchEvent(new CustomEvent('property-value-change'));
60+
this._submitModal();
61+
}
62+
63+
render(){
64+
return html`
65+
<umb-body-layout>
66+
<uui-box>
67+
<div>
68+
<p>
69+
<b>Connected: </b>${this.isTokenAvailable && this.isAuthorized()}
70+
</p>
71+
<p>
72+
<b>Account: </b>${this.isAuthorized() ? (this.data?.authResponse?.isFreeAccount ? "Free" : "Paid") : "N/A"}
73+
</p>
74+
${when(this.isAuthorized(), () => html`
75+
<p class="semrush-text-wrap">
76+
<b>Access Token: </b>
77+
<span>${this.token}</span>
78+
</p>
79+
`)}
80+
</div>
81+
82+
<uui-button label="Revoke" look="primary" ?disabled=${!this.isAuthorized()} @click=${this._revoke}></uui-button>
83+
</uui-box>
84+
85+
<uui-button slot="actions" label="Close" @click=${this._rejectModal}></uui-button>
86+
</umb-body-layout>
87+
`;
88+
}
89+
90+
static styles = [
91+
css`
92+
.semrush-text-wrap{
93+
word-break: break-all;
94+
white-space: normal;
95+
}
96+
`];
97+
}

0 commit comments

Comments
 (0)