Browse Source

Á few bug fixes

pull/1259/head
Sebastian Stehle 5 months ago
parent
commit
1e74211aeb
  1. 9
      backend/extensions/Squidex.Extensions/Actions/RuleEventPlugin.cs
  2. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs
  3. 4
      backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs
  4. 5
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs
  5. 65
      backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs
  6. 44
      backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs
  7. 2
      backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml
  8. 50
      frontend/package-lock.json
  9. 2
      frontend/package.json
  10. 2
      frontend/src/app/features/schemas/pages/schemas/schema-form.component.html
  11. 25
      frontend/src/app/framework/angular/forms/editors/date-time-editor.stories.ts
  12. 8
      frontend/src/app/framework/angular/forms/transform-input.directive.ts
  13. 1
      frontend/src/app/framework/internal.ts
  14. 4
      frontend/src/app/framework/utils/markdown-transform.ts
  15. 87
      frontend/src/app/framework/utils/slug.spec.ts
  16. 540
      frontend/src/app/framework/utils/slug.ts
  17. 10
      frontend/src/app/shared/state/assets.forms.spec.ts
  18. 5
      frontend/src/app/shared/state/assets.forms.ts

9
backend/extensions/Squidex.Extensions/Actions/RuleEventPlugin.cs

@ -1,4 +1,11 @@
using Microsoft.Extensions.Configuration;
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Entities.Backup;
using Squidex.Infrastructure.Plugins;

1
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/AssetScriptVars.cs

@ -6,6 +6,7 @@
// ==========================================================================
using System.Security.Claims;
using NodaTime;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Core.Scripting;

4
backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/Guards/ScriptingExtensions.cs

@ -155,6 +155,10 @@ public static class ScriptingExtensions
vars.AppId = operation.App.Id;
vars.AppName = operation.App.Name;
vars.ContentId = operation.CommandId;
vars.Created = operation.Snapshot.Created;
vars.CreatedBy = operation.Snapshot.CreatedBy;
vars.LastModified = operation.Snapshot.LastModified;
vars.LastModifiedBy = operation.Snapshot.LastModifiedBy;
vars.SchemaId = operation.Schema.Id;
vars.SchemaName = operation.Schema.Name;
vars.User = operation.User;

5
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ScriptContent.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Google.LongRunning;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Shared;
@ -68,8 +69,12 @@ public sealed class ScriptContent(IScriptEngine scriptEngine) : IContentEnricher
var vars = new ContentScriptVars
{
ContentId = content.Id,
Created = content.Created,
CreatedBy = content.CreatedBy,
Data = content.Data,
DataOld = default,
LastModified = content.LastModified,
LastModifiedBy = content.LastModifiedBy,
Status = content.Status,
StatusOld = default,
};

65
backend/src/Squidex/Areas/Api/Controllers/Users/UsersController.cs

@ -150,52 +150,55 @@ public sealed class UsersController(
try
{
var entity = await userResolver.FindByIdAsync(id, HttpContext.RequestAborted);
if (entity == null)
{
return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png");
}
if (entity != null)
if (entity.Claims.IsPictureUrlStored())
{
if (entity.Claims.IsPictureUrlStored())
var callback = new FileCallback(async (body, range, ct) =>
{
var callback = new FileCallback(async (body, range, ct) =>
try
{
try
{
await userPictureStore.DownloadAsync(entity.Id, body, ct);
}
catch
{
await body.WriteAsync(AvatarBytes, ct);
}
});
return new FileCallbackResult("image/png", callback);
}
await userPictureStore.DownloadAsync(entity.Id, body, ct);
}
catch
{
await body.WriteAsync(AvatarBytes, ct);
}
});
var httpClient = httpClientFactory.CreateClient("Users");
return new FileCallbackResult("image/png", callback);
}
var url = entity.Claims.PictureNormalizedUrl();
var httpClient = httpClientFactory.CreateClient("Users");
if (!string.IsNullOrWhiteSpace(url))
{
var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
var url = entity.Claims.PictureNormalizedUrl();
if (response.IsSuccessStatusCode)
{
var contentType = response.Content.Headers.ContentType?.ToString()!;
var contentStream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
if (!string.IsNullOrWhiteSpace(url))
{
var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
var etag = response.Headers.ETag;
if (response.IsSuccessStatusCode)
{
var contentType = response.Content.Headers.ContentType?.ToString()!;
var contentStream = await response.Content.ReadAsStreamAsync(HttpContext.RequestAborted);
var result = new FileStreamResult(contentStream, contentType);
var etag = response.Headers.ETag;
if (!string.IsNullOrWhiteSpace(etag?.Tag))
{
result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak);
}
var result = new FileStreamResult(contentStream, contentType);
return result;
if (!string.IsNullOrWhiteSpace(etag?.Tag))
{
result.EntityTag = new EntityTagHeaderValue(etag.Tag, etag.IsWeak);
}
return result;
}
}
return new FileStreamResult(new MemoryStream(AvatarBytes), "image/png");
}
catch (Exception ex)
{

44
backend/src/Squidex/Areas/IdentityServer/Controllers/Profile/ProfileController.cs

@ -123,7 +123,7 @@ public sealed class ProfileController(
[HttpPost]
[Route("account/profile/upload-picture/")]
public Task<IActionResult> UploadPicture(List<IFormFile> files)
public Task<IActionResult> UploadPicture([FromForm(Name = "file")] List<IFormFile> files)
{
return MakeChangeAsync((id, ct) => UpdatePictureAsync(files, id, ct),
T.Get("users.profile.uploadPictureDone"), None.Value);
@ -148,23 +148,41 @@ public sealed class ProfileController(
private async Task UpdatePictureAsync(List<IFormFile> files, string id,
CancellationToken ct)
{
if (files.Count != 1)
{
throw new ValidationException(T.Get("validation.onlyOneFile"));
}
var update = new UserValues
{
PictureUrl = SquidexClaimTypes.PictureUrlStore,
};
await UploadResizedAsync(files, id, ct);
await userService.UpdateAsync(id, update, ct: ct);
}
private async Task UploadResizedAsync(IAssetFile file, string id,
private async Task UploadResizedAsync(List<IFormFile> files, string id,
CancellationToken ct)
{
await using var assetResized = TempAssetFile.Create(file);
if (files.Count != 1)
{
throw new ValidationException(T.Get("validation.onlyOneFile"));
}
var file = files[0];
if (string.IsNullOrWhiteSpace(file.ContentType))
{
throw new ValidationException(T.Get("common.httpContentTypeNotDefined"));
}
if (string.IsNullOrWhiteSpace(file.FileName))
{
throw new ValidationException(T.Get("common.httpFileNameNotDefined"));
}
var assetFile = new DelegateAssetFile(
file.FileName,
file.ContentType,
file.Length,
file.OpenReadStream);
await using var assetResized = TempAssetFile.Create(assetFile);
var resizeOptions = new ResizeOptions
{
@ -174,13 +192,9 @@ public sealed class ProfileController(
try
{
await using (var originalStream = file.OpenRead())
{
await using (var resizeStream = assetResized.OpenWrite())
{
await assetGenerator.CreateThumbnailAsync(originalStream, file.MimeType, resizeStream, resizeOptions, ct);
}
}
await using var streamOriginal = assetFile.OpenRead();
await using var streamResized = assetResized.OpenWrite();
await assetGenerator.CreateThumbnailAsync(streamOriginal, assetFile.MimeType, streamResized, resizeOptions, ct);
}
catch
{

2
backend/src/Squidex/Areas/IdentityServer/Views/Profile/Profile.cshtml

@ -24,7 +24,7 @@
<div class="row profile-section-sm">
<div class="col profile-picture-col">
<img class="profile-picture" src="api/users/{Model.Id}/picture/?q={@Guid.NewGuid()}" />
<img class="profile-picture" src="api/users/@Model.Id/picture/?q=@Guid.NewGuid()" />
</div>
<div class="col">
<form id="pictureForm" class="profile-picture-form" asp-controller="Profile" asp-action="UploadPicture" method="post" enctype="multipart/form-data">

50
frontend/package-lock.json

@ -24,6 +24,7 @@
"@graphiql/toolkit": "^0.11.3",
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@lithiumjs/angular": "^8.0.1",
"@sindresorhus/slugify": "^3.0.0",
"ace-builds": "^1.42.0",
"angular-gridster2": "19.0.0",
"bootstrap": "5.3.6",
@ -49,7 +50,6 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"rxjs": "7.8.2",
"slugify": "1.6.6",
"textarea-caret": "github:component/textarea-caret-position",
"tslib": "2.8.1",
"tui-calendar": "^1.15.3",
@ -8995,6 +8995,34 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/slugify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-3.0.0.tgz",
"integrity": "sha512-SCrKh1zS96q+CuH5GumHcyQEVPsM4Ve8oE0E6tw7AAhGq50K8ojbTUOQnX/j9Mhcv/AXiIsbCfquovyGOo5fGw==",
"license": "MIT",
"dependencies": {
"@sindresorhus/transliterate": "^2.0.0",
"escape-string-regexp": "^5.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sindresorhus/transliterate": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-2.0.0.tgz",
"integrity": "sha512-lRx63oCHxeJ90DqIgmbxH1PQmiBDY1wVaLzB4hK0d/xS5BrG1iZO3HdCJS/DQJk6GJ8xHDev8OMI7iGxvE1ZUA==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
@ -14698,6 +14726,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/escape-string-regexp": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@ -22783,14 +22823,6 @@
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slugify": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz",
"integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",

2
frontend/package.json

@ -31,6 +31,7 @@
"@graphiql/toolkit": "^0.11.3",
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@lithiumjs/angular": "^8.0.1",
"@sindresorhus/slugify": "^3.0.0",
"ace-builds": "^1.42.0",
"angular-gridster2": "19.0.0",
"bootstrap": "5.3.6",
@ -56,7 +57,6 @@
"react": "18.3.1",
"react-dom": "18.3.1",
"rxjs": "7.8.2",
"slugify": "1.6.6",
"textarea-caret": "github:component/textarea-caret-position",
"tslib": "2.8.1",
"tui-calendar": "^1.15.3",

2
frontend/src/app/features/schemas/pages/schemas/schema-form.component.html

@ -128,7 +128,7 @@
}
}
} @else if (selectedTab === 1) {
<sqx-code-editor formControlName="importing" [height]="1000" valueMode="Json" />
<sqx-code-editor formControlName="importing" height="auto" minLines="50" valueMode="Json" />
} @else if (selectedTab === 2) {
<div class="row g-2 form-group">
<div class="col">

25
frontend/src/app/framework/angular/forms/editors/date-time-editor.stories.ts

@ -59,16 +59,21 @@ export default {
render: args => ({
props: args,
template: `
<sqx-date-time-editor
[disabled]="disabled"
[hideClear]="hideClear"
[hideDateButtons]="hideDateButtons"
[hideDateTimeModeButton]="hideDateTimeModeButton"
[mode]="mode"
(ngModelChange)="change($event)"
[ngModel]="ngModel"
[size]="size">
</sqx-date-time-editor>
<div>
<sqx-date-time-editor
[disabled]="disabled"
[hideClear]="hideClear"
[hideDateButtons]="hideDateButtons"
[hideDateTimeModeButton]="hideDateTimeModeButton"
[mode]="mode"
(ngModelChange)="ngModel = $event; change($event)"
[ngModel]="ngModel"
[size]="size">
</sqx-date-time-editor>
<div style="margin-bottom: 0.5rem; font-family: monospace; color: #555;">
ngModel: {{ ngModel }}
</div>
</div>
`,
}),
decorators: [

8
frontend/src/app/framework/angular/forms/transform-input.directive.ts

@ -7,15 +7,13 @@
import { Directive, ElementRef, forwardRef, HostListener, Input, Renderer2 } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import slugify from 'slugify';
import { Types } from '@app/framework/internal';
import { generateSlug, Types } from '@app/framework/internal';
type Transform = (value: string) => string;
export const TransformNoop: Transform = value => value;
export const TransformLowerCase: Transform = value => value.toLowerCase();
export const TransformSlugify: Transform = value => slugify(value, { lower: true, trim: false });
export const TransformSlugifyCased: Transform = value => slugify(value, { lower: false, trim: false });
export const TransformSlugify: Transform = value => generateSlug(value, { preserveTrailingDash: true });
export const TransformUpperCase: Transform = value => value.toUpperCase();
export const SQX_TRANSFORM_INPUT_VALUE_ACCESSOR: any = {
@ -40,8 +38,6 @@ export class TransformInputDirective implements ControlValueAccessor {
this.transformer = TransformLowerCase;
} else if (value === 'Slugify') {
this.transform = TransformSlugify;
} else if (value === 'SlugifyCased') {
this.transform = TransformSlugifyCased;
} else if (value === 'UpperCase') {
this.transform = TransformUpperCase;
}

1
frontend/src/app/framework/internal.ts

@ -42,6 +42,7 @@ export * from './utils/math-helper';
export * from './utils/modal-view';
export * from './utils/picasso';
export * from './utils/rxjs-extensions';
export * from './utils/slug';
export * from './utils/string-helper';
export * from './utils/tag-values';
export * from './utils/text-measurer';

4
frontend/src/app/framework/utils/markdown-transform.ts

@ -5,8 +5,8 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import slugify from 'slugify';
import { MathHelper } from './math-helper';
import { generateSlug } from './slug';
const IMAGE_REGEX = /!\[(?<alt>[^\]]*)\]\((?<url>.*?)([\s]["\s]*(?<name>[^")]*)["\s]*)?\)/;
const IMAGES_REGEX = /!\[(?<alt>[^\]]*)\]\((?<url>.*?)([\s]["\s]*(?<name>[^")]*)["\s]*)?\)/g;
@ -74,7 +74,7 @@ const IMAGE_EXTENSIONS = ['.avif', '.jpeg', '.jpg', '.png', '.webp'];
function toImage(image: { url: string; name?: string; alt?: string }): MarkdownImage {
let name = image.name || image.alt || 'image';
name = slugify(name, { lower: true, trim: true });
name = generateSlug(name);
if (!IMAGE_EXTENSIONS.find(ex => name.endsWith(ex))) {
name += '.webp';

87
frontend/src/app/framework/utils/slug.spec.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { generateSlug } from './slug';
describe('generateSlug', () => {
describe('Should replace special characters with separator', () => {
const cases: Array<[string, string, string]> = [
['Hello World', '-', 'hello-world'],
['Hello/World', '-', 'hello-world'],
['Hello World', '_', 'hello_world'],
['Hello/World', '_', 'hello_world'],
['Hello World ', '_', 'hello_world'],
['Hello World-', '_', 'hello_world'],
['Hello/World_', '_', 'hello_world'],
];
cases.forEach(([input, separator, expected]) => {
it(`slugifies "${input}" with separator "${separator}" to "${expected}"`, () => {
expect(generateSlug(input, { separator })).toEqual(expected);
});
});
});
describe('Should replace multi-char diacritics', () => {
const cases: Array<[string, string]> = [
['ö', 'oe'],
['ü', 'ue'],
['ä', 'ae'],
];
cases.forEach(([input, expected]) => {
it(`slugifies "${input}" to "${expected}"`, () => {
expect(generateSlug(input)).toEqual(expected);
});
});
});
describe('Should not replace multi-char diacritics', () => {
const cases: Array<[string, string]> = [
['ö', 'o'],
['ü', 'u'],
['ä', 'a'],
];
cases.forEach(([input, expected]) => {
it(`slugifies "${input}" to "${expected}" with singleCharDiacritic=true`, () => {
expect(generateSlug(input, { singleCharDiacritic: true })).toEqual(expected);
});
});
});
describe('Should replace single-char diacritics', () => {
const cases: Array<[string, string]> = [
['Físh', 'fish'],
['źish', 'zish'],
['żish', 'zish'],
['fórm', 'form'],
['fòrm', 'form'],
['fårt', 'fart'],
];
cases.forEach(([input, expected]) => {
it(`slugifies "${input}" to "${expected}"`, () => {
expect(generateSlug(input)).toEqual(expected);
});
});
});
describe('Should keep characters', () => {
const cases: Array<[string, string, string]> = [
['Hello my&World ', '_', 'hello_my&world'],
['Hello my&World-', '_', 'hello_my&world'],
['Hello my/World_', '_', 'hello_my/world'],
];
cases.forEach(([input, separator, expected]) => {
it(`slugifies "${input}" with separator "${separator}" keeping chars`, () => {
expect(generateSlug(input, { allowed: ['&', '/'], separator })).toEqual(expected);
});
});
});
});

540
frontend/src/app/framework/utils/slug.ts

@ -0,0 +1,540 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import slugify from '@sindresorhus/slugify';
const DIACRITICS: Record<string, string> = {
'$': 'dollar',
'%': 'percent',
'&': 'and',
'<': 'less',
'>': 'greater',
'|': 'or',
'¢': 'cent',
'£': 'pound',
'¤': 'currency',
'¥': 'yen',
'©': '(c)',
'ª': 'a',
'®': '(r)',
'º': 'o',
'À': 'A',
'Á': 'A',
'Â': 'A',
'Ã': 'A',
'Ä': 'AE',
'Å': 'A',
'Æ': 'AE',
'Ç': 'C',
'Ə': 'E',
'È': 'E',
'É': 'E',
'Ê': 'E',
'Ë': 'E',
'Ì': 'I',
'Í': 'I',
'Î': 'I',
'Ï': 'I',
'Ð': 'D',
'Ñ': 'N',
'Ò': 'O',
'Ó': 'O',
'Ô': 'O',
'Õ': 'O',
'Ö': 'OE',
'Ø': 'O',
'Ù': 'U',
'Ú': 'U',
'Û': 'U',
'Ü': 'UE',
'Ý': 'Y',
'Þ': 'TH',
'ß': 'ss',
'à': 'a',
'á': 'a',
'â': 'a',
'ã': 'a',
'ä': 'ae',
'å': 'a',
'æ': 'ae',
'ç': 'c',
'ə': 'e',
'è': 'e',
'é': 'e',
'ê': 'e',
'ë': 'e',
'ì': 'i',
'í': 'i',
'î': 'i',
'ï': 'i',
'ð': 'd',
'ñ': 'n',
'ò': 'o',
'ó': 'o',
'ô': 'o',
'õ': 'o',
'ö': 'oe',
'ø': 'o',
'ù': 'u',
'ú': 'u',
'û': 'u',
'ü': 'ue',
'ý': 'y',
'þ': 'th',
'ÿ': 'y',
'Ā': 'A',
'ā': 'a',
'Ă': 'A',
'ă': 'a',
'Ą': 'A',
'ą': 'a',
'Ć': 'C',
'ć': 'c',
'Č': 'C',
'č': 'c',
'Ď': 'D',
'ď': 'd',
'Đ': 'DJ',
'đ': 'dj',
'Ē': 'E',
'ē': 'e',
'Ė': 'E',
'ė': 'e',
'Ę': 'e',
'ę': 'e',
'Ě': 'E',
'ě': 'e',
'Ğ': 'G',
'ğ': 'g',
'Ģ': 'G',
'ģ': 'g',
'Ĩ': 'I',
'ĩ': 'i',
'Ī': 'i',
'ī': 'i',
'Į': 'I',
'į': 'i',
'İ': 'I',
'ı': 'i',
'Ķ': 'k',
'ķ': 'k',
'Ļ': 'L',
'ļ': 'l',
'Ľ': 'L',
'ľ': 'l',
'Ł': 'L',
'ł': 'l',
'Ń': 'N',
'ń': 'n',
'Ņ': 'N',
'ņ': 'n',
'Ň': 'N',
'ň': 'n',
'Ő': 'O',
'ő': 'o',
'Œ': 'OE',
'œ': 'oe',
'Ŕ': 'R',
'ŕ': 'r',
'Ř': 'R',
'ř': 'r',
'Ś': 'S',
'ś': 's',
'Ş': 'S',
'ş': 's',
'Š': 'S',
'š': 's',
'Ţ': 'T',
'ţ': 't',
'Ť': 'T',
'ť': 't',
'Ũ': 'U',
'ũ': 'u',
'Ū': 'u',
'ū': 'u',
'Ů': 'U',
'ů': 'u',
'Ű': 'U',
'ű': 'u',
'Ų': 'U',
'ų': 'u',
'Ź': 'Z',
'ź': 'z',
'Ż': 'Z',
'ż': 'z',
'Ž': 'Z',
'ž': 'z',
'ƒ': 'f',
'Ơ': 'O',
'ơ': 'o',
'Ư': 'U',
'ư': 'u',
'Lj': 'LJ',
'lj': 'lj',
'Nj': 'NJ',
'nj': 'nj',
'Ș': 'S',
'ș': 's',
'Ț': 'T',
'ț': 't',
'˚': 'o',
'Ά': 'A',
'Έ': 'E',
'Ή': 'H',
'Ί': 'I',
'Ό': 'O',
'Ύ': 'Y',
'Ώ': 'W',
'ΐ': 'i',
'Α': 'A',
'Β': 'B',
'Γ': 'G',
'Δ': 'D',
'Ε': 'E',
'Ζ': 'Z',
'Η': 'H',
'Θ': '8',
'Ι': 'I',
'Κ': 'K',
'Λ': 'L',
'Μ': 'M',
'Ν': 'N',
'Ξ': '3',
'Ο': 'O',
'Π': 'P',
'Ρ': 'R',
'Σ': 'S',
'Τ': 'T',
'Υ': 'Y',
'Φ': 'F',
'Χ': 'X',
'Ψ': 'PS',
'Ω': 'W',
'Ϊ': 'I',
'Ϋ': 'Y',
'ά': 'a',
'έ': 'e',
'ή': 'h',
'ί': 'i',
'ΰ': 'y',
'α': 'a',
'β': 'b',
'γ': 'g',
'δ': 'd',
'ε': 'e',
'ζ': 'z',
'η': 'h',
'θ': '8',
'ι': 'i',
'κ': 'k',
'λ': 'l',
'μ': 'm',
'ν': 'n',
'ξ': '3',
'ο': 'o',
'π': 'p',
'ρ': 'r',
'ς': 's',
'σ': 's',
'τ': 't',
'υ': 'y',
'φ': 'f',
'χ': 'x',
'ψ': 'ps',
'ω': 'w',
'ϊ': 'i',
'ϋ': 'y',
'ό': 'o',
'ύ': 'y',
'ώ': 'w',
'Ё': 'Yo',
'Ђ': 'DJ',
'Є': 'Ye',
'І': 'I',
'Ї': 'Yi',
'Ј': 'J',
'Љ': 'LJ',
'Њ': 'NJ',
'Ћ': 'C',
'Џ': 'DZ',
'А': 'A',
'Б': 'B',
'В': 'V',
'Г': 'G',
'Д': 'D',
'Е': 'E',
'Ж': 'Zh',
'З': 'Z',
'И': 'I',
'Й': 'J',
'К': 'K',
'Л': 'L',
'М': 'M',
'Н': 'N',
'О': 'O',
'П': 'P',
'Р': 'R',
'С': 'S',
'Т': 'T',
'У': 'U',
'Ф': 'F',
'Х': 'H',
'Ц': 'C',
'Ч': 'Ch',
'Ш': 'Sh',
'Щ': 'Sh',
'Ъ': 'U',
'Ы': 'Y',
'Ь': 'b',
'Э': 'E',
'Ю': 'Yu',
'Я': 'Ya',
'а': 'a',
'б': 'b',
'в': 'v',
'г': 'g',
'д': 'd',
'е': 'e',
'ж': 'zh',
'з': 'z',
'и': 'i',
'й': 'j',
'к': 'k',
'л': 'l',
'м': 'm',
'н': 'n',
'о': 'o',
'п': 'p',
'р': 'r',
'с': 's',
'т': 't',
'у': 'u',
'ф': 'f',
'х': 'h',
'ц': 'c',
'ч': 'ch',
'ш': 'sh',
'щ': 'sh',
'ъ': 'u',
'ы': 'y',
'ь': 's',
'э': 'e',
'ю': 'yu',
'я': 'ya',
'ё': 'yo',
'ђ': 'dj',
'є': 'ye',
'і': 'i',
'ї': 'yi',
'ј': 'j',
'љ': 'lj',
'њ': 'nj',
'ћ': 'c',
'џ': 'dz',
'Ґ': 'G',
'ґ': 'g',
'฿': 'baht',
'ა': 'a',
'ბ': 'b',
'გ': 'g',
'დ': 'd',
'ე': 'e',
'ვ': 'v',
'ზ': 'z',
'თ': 't',
'ი': 'i',
'კ': 'k',
'ლ': 'l',
'მ': 'm',
'ნ': 'n',
'ო': 'o',
'პ': 'p',
'ჟ': 'zh',
'რ': 'r',
'ს': 's',
'ტ': 't',
'უ': 'u',
'ფ': 'f',
'ქ': 'k',
'ღ': 'gh',
'ყ': 'q',
'შ': 'sh',
'ჩ': 'ch',
'ც': 'ts',
'ძ': 'dz',
'წ': 'ts',
'ჭ': 'ch',
'ხ': 'kh',
'ჯ': 'j',
'ჰ': 'h',
'ẞ': 'SS',
'Ạ': 'A',
'ạ': 'a',
'Ả': 'A',
'ả': 'a',
'Ấ': 'A',
'ấ': 'a',
'Ầ': 'A',
'ầ': 'a',
'Ẩ': 'A',
'ẩ': 'a',
'Ẫ': 'A',
'ẫ': 'a',
'Ậ': 'A',
'ậ': 'a',
'Ắ': 'A',
'ắ': 'a',
'Ằ': 'A',
'ằ': 'a',
'Ẳ': 'A',
'ẳ': 'a',
'Ẵ': 'A',
'ẵ': 'a',
'Ặ': 'A',
'ặ': 'a',
'Ẹ': 'E',
'ẹ': 'e',
'Ẻ': 'E',
'ẻ': 'e',
'Ẽ': 'E',
'ẽ': 'e',
'Ế': 'E',
'ế': 'e',
'Ề': 'E',
'ề': 'e',
'Ể': 'E',
'ể': 'e',
'Ễ': 'E',
'ễ': 'e',
'Ệ': 'E',
'ệ': 'e',
'Ỉ': 'I',
'ỉ': 'i',
'Ị': 'I',
'ị': 'i',
'Ọ': 'O',
'ọ': 'o',
'Ỏ': 'O',
'ỏ': 'o',
'Ố': 'O',
'ố': 'o',
'Ồ': 'O',
'ồ': 'o',
'Ổ': 'O',
'ổ': 'o',
'Ỗ': 'O',
'ỗ': 'o',
'Ộ': 'O',
'ộ': 'o',
'Ớ': 'O',
'ớ': 'o',
'Ờ': 'O',
'ờ': 'o',
'Ở': 'O',
'ở': 'o',
'Ỡ': 'O',
'ỡ': 'o',
'Ợ': 'O',
'ợ': 'o',
'Ụ': 'U',
'ụ': 'u',
'Ủ': 'U',
'ủ': 'u',
'Ứ': 'U',
'ứ': 'u',
'Ừ': 'U',
'ừ': 'u',
'Ử': 'U',
'ử': 'u',
'Ữ': 'U',
'ữ': 'u',
'Ự': 'U',
'ự': 'u',
'Ỳ': 'Y',
'ỳ': 'y',
'Ỵ': 'Y',
'ỵ': 'y',
'Ỷ': 'Y',
'ỷ': 'y',
'Ỹ': 'Y',
'ỹ': 'y',
'‘': '\'',
'’': '\'',
'“': '\\\"',
'”': '\\\"',
'†': '+',
'•': '*',
'…': '...',
'₠': 'ecu',
'₢': 'cruzeiro',
'₣': 'french franc',
'₤': 'lira',
'₥': 'mill',
'₦': 'naira',
'₧': 'peseta',
'₨': 'rupee',
'₩': 'won',
'₪': 'new shequel',
'₫': 'dong',
'€': 'euro',
'₭': 'kip',
'₮': 'tugrik',
'₯': 'drachma',
'₰': 'penny',
'₱': 'peso',
'₲': 'guarani',
'₳': 'austral',
'₴': 'hryvnia',
'₵': 'cedi',
'₹': 'indian rupee',
'₽': 'russian ruble',
'₿': 'bitcoin',
'℠': 'sm',
'™': 'tm',
'∂': 'd',
'∆': 'delta',
'∑': 'sum',
'∞': 'infinity',
'♥': 'love',
'元': 'yuan',
'円': 'yen',
'﷼': 'rial',
};
export const LOWER_CASE_DIACRITICS: ReadonlyArray<[string, string]> =
Object.entries(DIACRITICS).map(([key, value]) => [key, value.toLowerCase()]);
export const LOWER_CASE_SINGLE_DIACRITICS: ReadonlyArray<[string, string]> =
Object.entries(DIACRITICS).map(([key, value]) => [key, value.toLowerCase()[0]]);
export function generateSlug(input: string, options?: { separator?: string; singleCharDiacritic?: boolean; allowed?: string[]; preserveTrailingDash?: boolean }) {
if (!input) {
return input?.trim();
}
const separator = options?.separator || '-';
let customReplacements =
options?.singleCharDiacritic ?
LOWER_CASE_SINGLE_DIACRITICS :
LOWER_CASE_DIACRITICS;
if (options?.allowed) {
customReplacements = [...customReplacements];
for (const key of options.allowed) {
customReplacements = customReplacements.filter(x => x[0] !== key);
}
}
return slugify(input, {
customReplacements,
preserveCharacters: options?.allowed || [],
preserveTrailingDash: options?.preserveTrailingDash,
separator,
transliterate: false,
});
}

10
frontend/src/app/shared/state/assets.forms.spec.ts

@ -58,6 +58,16 @@ describe('AnnotateAssetForm', () => {
expect(slug).toBe('my-new-file.png');
});
it('should create slug from file with dots', () => {
form.form.get('fileName')!.setValue('My.New.File');
form.generateSlug(asset);
const slug = form.form.get('slug')!.value;
expect(slug).toBe('my.new.file.png');
});
it('should convert metadata if loading', () => {
form.load(asset);

5
frontend/src/app/shared/state/assets.forms.ts

@ -6,8 +6,7 @@
*/
import { UntypedFormControl, Validators } from '@angular/forms';
import slugify from 'slugify';
import { ExtendedFormGroup, Form, TemplatedFormArray, Types } from '@app/framework';
import { ExtendedFormGroup, Form, generateSlug, TemplatedFormArray, Types } from '@app/framework';
import { AnnotateAssetDto, AssetDto, AssetFolderDto, MoveAssetDto, RenameAssetFolderDto, RenameTagDto, UpdateAssetScriptsDto } from '../model';
export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto, AssetDto> {
@ -144,7 +143,7 @@ export class AnnotateAssetForm extends Form<ExtendedFormGroup, AnnotateAssetDto,
const fileName = this.form.controls['fileName'].value;
if (fileName) {
let slug = slugify(fileName, { lower: true });
let slug = generateSlug(fileName, { allowed: ['.'] });
if (asset.fileName) {
const index = asset.fileName.lastIndexOf('.');

Loading…
Cancel
Save