feat: Improve org name and slug validation (#1924)
- Verifies org slug (name) availability when creating new org - Show org max length error when signing up - Highlights org error field when signing up - Fixes org name max length discrepancy - Standardizes org slug to lowercase
This commit is contained in:
commit
5e9e897713
@ -1,4 +1,5 @@
|
|||||||
import { localized, msg, str } from "@lit/localize";
|
import { localized, msg, str } from "@lit/localize";
|
||||||
|
import type { SlInput, SlInputEvent } from "@shoelace-style/shoelace";
|
||||||
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
||||||
import { type PropertyValues, type TemplateResult } from "lit";
|
import { type PropertyValues, type TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
@ -11,6 +12,7 @@ import type { AuthState } from "@/utils/AuthService";
|
|||||||
import { maxLengthValidator } from "@/utils/form";
|
import { maxLengthValidator } from "@/utils/form";
|
||||||
import LiteElement, { html } from "@/utils/LiteElement";
|
import LiteElement, { html } from "@/utils/LiteElement";
|
||||||
import type { OrgData } from "@/utils/orgs";
|
import type { OrgData } from "@/utils/orgs";
|
||||||
|
import slugifyStrict from "@/utils/slugify";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @fires btrix-update-user-info
|
* @fires btrix-update-user-info
|
||||||
@ -30,6 +32,9 @@ export class Home extends LiteElement {
|
|||||||
@state()
|
@state()
|
||||||
private orgList?: OrgData[];
|
private orgList?: OrgData[];
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private orgSlugs: string[] = [];
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isAddingOrg = false;
|
private isAddingOrg = false;
|
||||||
|
|
||||||
@ -39,7 +44,10 @@ export class Home extends LiteElement {
|
|||||||
@state()
|
@state()
|
||||||
private isSubmittingNewOrg = false;
|
private isSubmittingNewOrg = false;
|
||||||
|
|
||||||
private readonly validateOrgNameMax = maxLengthValidator(50);
|
@state()
|
||||||
|
private isOrgNameValid: boolean | null = null;
|
||||||
|
|
||||||
|
private readonly validateOrgNameMax = maxLengthValidator(40);
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
if (this.authState) {
|
if (this.authState) {
|
||||||
@ -171,6 +179,29 @@ export class Home extends LiteElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${this.renderAddOrgDialog()}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderAddOrgDialog() {
|
||||||
|
let orgNameStatusLabel = msg("Start typing to see availability");
|
||||||
|
let orgNameStatusIcon = html`
|
||||||
|
<sl-icon class="mr-3 text-neutral-300" name="check-lg"></sl-icon>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (this.isOrgNameValid) {
|
||||||
|
orgNameStatusLabel = msg("This org name is available");
|
||||||
|
orgNameStatusIcon = html`
|
||||||
|
<sl-icon class="mr-3 text-success" name="check-lg"></sl-icon>
|
||||||
|
`;
|
||||||
|
} else if (this.isOrgNameValid === false) {
|
||||||
|
orgNameStatusLabel = msg("This org name is taken");
|
||||||
|
orgNameStatusIcon = html`
|
||||||
|
<sl-icon class="mr-3 text-danger" name="x-lg"></sl-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
<btrix-dialog
|
<btrix-dialog
|
||||||
.label=${msg("New Organization")}
|
.label=${msg("New Organization")}
|
||||||
.open=${this.isAddingOrg}
|
.open=${this.isAddingOrg}
|
||||||
@ -183,7 +214,10 @@ export class Home extends LiteElement {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@sl-show=${() => (this.isAddOrgFormVisible = true)}
|
@sl-show=${() => (this.isAddOrgFormVisible = true)}
|
||||||
@sl-after-hide=${() => (this.isAddOrgFormVisible = false)}
|
@sl-after-hide=${() => {
|
||||||
|
this.isAddOrgFormVisible = false;
|
||||||
|
this.isOrgNameValid = null;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
${this.isAddOrgFormVisible
|
${this.isAddOrgFormVisible
|
||||||
? html`
|
? html`
|
||||||
@ -201,8 +235,17 @@ export class Home extends LiteElement {
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
required
|
required
|
||||||
help-text=${this.validateOrgNameMax.helpText}
|
help-text=${this.validateOrgNameMax.helpText}
|
||||||
@sl-input=${this.validateOrgNameMax.validate}
|
@sl-input=${this.onOrgNameInput}
|
||||||
>
|
>
|
||||||
|
<sl-tooltip
|
||||||
|
slot="suffix"
|
||||||
|
content=${orgNameStatusLabel}
|
||||||
|
@sl-hide=${(e: CustomEvent) => e.stopPropagation()}
|
||||||
|
@sl-after-hide=${(e: CustomEvent) => e.stopPropagation()}
|
||||||
|
hoist
|
||||||
|
>
|
||||||
|
${orgNameStatusIcon}
|
||||||
|
</sl-tooltip>
|
||||||
</sl-input>
|
</sl-input>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -277,7 +320,12 @@ export class Home extends LiteElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async fetchOrgs() {
|
private async fetchOrgs() {
|
||||||
this.orgList = await this.getOrgs();
|
try {
|
||||||
|
this.orgList = await this.getOrgs();
|
||||||
|
this.orgSlugs = await this.getOrgSlugs();
|
||||||
|
} catch (e) {
|
||||||
|
console.debug(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getOrgs() {
|
private async getOrgs() {
|
||||||
@ -289,6 +337,31 @@ export class Home extends LiteElement {
|
|||||||
return data.items;
|
return data.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getOrgSlugs() {
|
||||||
|
const data = await this.apiFetch<{ slugs: string[] }>(
|
||||||
|
"/orgs/slugs",
|
||||||
|
this.authState!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return data.slugs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onOrgNameInput(e: SlInputEvent) {
|
||||||
|
this.validateOrgNameMax.validate(e);
|
||||||
|
|
||||||
|
const input = e.target as SlInput;
|
||||||
|
const slug = slugifyStrict(input.value);
|
||||||
|
const isInvalid = this.orgSlugs.includes(slug);
|
||||||
|
|
||||||
|
if (isInvalid) {
|
||||||
|
input.setCustomValidity(msg("This org name is already taken."));
|
||||||
|
} else {
|
||||||
|
input.setCustomValidity("");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isOrgNameValid = !isInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
private async onSubmitNewOrg(e: SubmitEvent) {
|
private async onSubmitNewOrg(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@ -318,10 +391,24 @@ export class Home extends LiteElement {
|
|||||||
});
|
});
|
||||||
this.isAddingOrg = false;
|
this.isAddingOrg = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
let message = msg("Sorry, couldn't create organization at this time.");
|
||||||
|
|
||||||
|
if (isApiError(e)) {
|
||||||
|
if (e.details === "duplicate_org_name") {
|
||||||
|
message = msg("This org name is already taken, try another one.");
|
||||||
|
} else if (e.details === "duplicate_org_slug") {
|
||||||
|
message = msg(
|
||||||
|
"This org URL identifier is already taken, try another one.",
|
||||||
|
);
|
||||||
|
} else if (e.details === "invalid_slug") {
|
||||||
|
message = msg(
|
||||||
|
"This org URL identifier is invalid. Please use alphanumeric characters and dashes (-) only.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.notify({
|
this.notify({
|
||||||
message: isApiError(e)
|
message,
|
||||||
? e.message
|
|
||||||
: msg("Sorry, couldn't create organization at this time."),
|
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
icon: "exclamation-octagon",
|
icon: "exclamation-octagon",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import { Task, TaskStatus } from "@lit/task";
|
|||||||
import type { SlInput } from "@shoelace-style/shoelace";
|
import type { SlInput } from "@shoelace-style/shoelace";
|
||||||
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
|
||||||
import { html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property, query } from "lit/decorators.js";
|
||||||
import slugify from "slugify";
|
|
||||||
|
|
||||||
import { TailwindElement } from "@/classes/TailwindElement";
|
import { TailwindElement } from "@/classes/TailwindElement";
|
||||||
import { APIController } from "@/controllers/api";
|
import { APIController } from "@/controllers/api";
|
||||||
@ -12,6 +11,8 @@ import { NotifyController } from "@/controllers/notify";
|
|||||||
import { type APIUser } from "@/index";
|
import { type APIUser } from "@/index";
|
||||||
import { isApiError } from "@/utils/api";
|
import { isApiError } from "@/utils/api";
|
||||||
import type { AuthState } from "@/utils/AuthService";
|
import type { AuthState } from "@/utils/AuthService";
|
||||||
|
import { maxLengthValidator } from "@/utils/form";
|
||||||
|
import slugifyStrict from "@/utils/slugify";
|
||||||
import { AppStateService } from "@/utils/state";
|
import { AppStateService } from "@/utils/state";
|
||||||
import { formatAPIUser } from "@/utils/user";
|
import { formatAPIUser } from "@/utils/user";
|
||||||
|
|
||||||
@ -42,9 +43,14 @@ export class OrgForm extends TailwindElement {
|
|||||||
@property({ type: String })
|
@property({ type: String })
|
||||||
slug = "";
|
slug = "";
|
||||||
|
|
||||||
|
@query("#orgForm")
|
||||||
|
private readonly form?: HTMLFormElement | null;
|
||||||
|
|
||||||
readonly _api = new APIController(this);
|
readonly _api = new APIController(this);
|
||||||
readonly _notify = new NotifyController(this);
|
readonly _notify = new NotifyController(this);
|
||||||
|
|
||||||
|
private readonly validateOrgNameMax = maxLengthValidator(40);
|
||||||
|
|
||||||
readonly _renameOrgTask = new Task(this, {
|
readonly _renameOrgTask = new Task(this, {
|
||||||
autoRun: false,
|
autoRun: false,
|
||||||
task: async ([id, name, slug]) => {
|
task: async ([id, name, slug]) => {
|
||||||
@ -67,20 +73,20 @@ export class OrgForm extends TailwindElement {
|
|||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<sl-input
|
<sl-input
|
||||||
name="orgName"
|
name="orgName"
|
||||||
label=${msg("Org name")}
|
label=${msg("Org Name")}
|
||||||
placeholder=${msg("My Organization")}
|
placeholder=${msg("My Organization")}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
value=${this.name === this.orgId ? "" : this.name}
|
value=${this.name === this.orgId ? "" : this.name}
|
||||||
minlength="2"
|
minlength="2"
|
||||||
maxlength="40"
|
|
||||||
help-text=${msg("You can change this in your org settings later.")}
|
help-text=${msg("You can change this in your org settings later.")}
|
||||||
required
|
required
|
||||||
|
@sl-input=${this.validateOrgNameMax.validate}
|
||||||
></sl-input>
|
></sl-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<sl-input
|
<sl-input
|
||||||
name="orgSlug"
|
name="orgSlug"
|
||||||
label=${msg("Custom URL identifier")}
|
label=${msg("Custom URL Identifier")}
|
||||||
placeholder="my-organization"
|
placeholder="my-organization"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
value=${this.slug}
|
value=${this.slug}
|
||||||
@ -90,7 +96,7 @@ export class OrgForm extends TailwindElement {
|
|||||||
required
|
required
|
||||||
@sl-input=${(e: InputEvent) => {
|
@sl-input=${(e: InputEvent) => {
|
||||||
const input = e.target as SlInput;
|
const input = e.target as SlInput;
|
||||||
input.helpText = helpText(slugify(input.value, { strict: true }));
|
input.helpText = helpText(slugifyStrict(input.value));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
</sl-input>
|
</sl-input>
|
||||||
@ -123,7 +129,7 @@ export class OrgForm extends TailwindElement {
|
|||||||
|
|
||||||
const params = serialize(form) as FormValues;
|
const params = serialize(form) as FormValues;
|
||||||
const orgName = params.orgName;
|
const orgName = params.orgName;
|
||||||
const orgSlug = slugify(params.orgSlug, { strict: true });
|
const orgSlug = slugifyStrict(params.orgSlug);
|
||||||
|
|
||||||
void this._renameOrgTask.run([this.orgId, orgName, orgSlug]);
|
void this._renameOrgTask.run([this.orgId, orgName, orgSlug]);
|
||||||
}
|
}
|
||||||
@ -148,20 +154,35 @@ export class OrgForm extends TailwindElement {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.debug(e);
|
console.debug(e);
|
||||||
if (isApiError(e)) {
|
if (isApiError(e)) {
|
||||||
|
let error: Error | null = null;
|
||||||
|
let fieldName = "";
|
||||||
|
|
||||||
if (e.details === "duplicate_org_name") {
|
if (e.details === "duplicate_org_name") {
|
||||||
throw new Error(
|
fieldName = "orgName";
|
||||||
msg("This org name is already taken, try another one."),
|
error = new Error(
|
||||||
|
msg(str`The org name "${name}" is already taken, try another one.`),
|
||||||
);
|
);
|
||||||
} else if (e.details === "duplicate_org_slug") {
|
} else if (e.details === "duplicate_org_slug") {
|
||||||
throw new Error(
|
fieldName = "orgSlug";
|
||||||
msg("This org URL is already taken, try another one."),
|
error = new Error(
|
||||||
);
|
|
||||||
} else if (e.details === "invalid_slug") {
|
|
||||||
throw new Error(
|
|
||||||
msg(
|
msg(
|
||||||
"This org URL is invalid. Please use alphanumeric characters and dashes (-) only.",
|
str`The org URL identifier "${slug}" is already taken, try another one.`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
} else if (e.details === "invalid_slug") {
|
||||||
|
fieldName = "orgSlug";
|
||||||
|
error = new Error(
|
||||||
|
msg(
|
||||||
|
str`The org URL identifier "${slug}" is not a valid URL. Please use alphanumeric characters and dashes (-) only`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (fieldName) {
|
||||||
|
this.highlightErrorField(fieldName, error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +196,20 @@ export class OrgForm extends TailwindElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private highlightErrorField(fieldName: string, error: Error) {
|
||||||
|
const input = this.form?.querySelector<SlInput>(`[name="${fieldName}"]`);
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.setCustomValidity(error.message);
|
||||||
|
|
||||||
|
const onOneInput = () => {
|
||||||
|
input.setCustomValidity("");
|
||||||
|
input.removeEventListener("sl-input", onOneInput);
|
||||||
|
};
|
||||||
|
input.addEventListener("sl-input", onOneInput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async checkFormValidity(formEl: HTMLFormElement) {
|
private async checkFormValidity(formEl: HTMLFormElement) {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
return !formEl.querySelector("[data-invalid]");
|
return !formEl.querySelector("[data-invalid]");
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { html, type PropertyValues } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators.js";
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
import { ifDefined } from "lit/directives/if-defined.js";
|
import { ifDefined } from "lit/directives/if-defined.js";
|
||||||
import { when } from "lit/directives/when.js";
|
import { when } from "lit/directives/when.js";
|
||||||
import slugify from "slugify";
|
|
||||||
|
|
||||||
import { columns } from "./ui/columns";
|
import { columns } from "./ui/columns";
|
||||||
|
|
||||||
@ -20,6 +19,7 @@ import { isApiError } from "@/utils/api";
|
|||||||
import type { AuthState } from "@/utils/AuthService";
|
import type { AuthState } from "@/utils/AuthService";
|
||||||
import { maxLengthValidator } from "@/utils/form";
|
import { maxLengthValidator } from "@/utils/form";
|
||||||
import { AccessCode, isAdmin, isCrawler, type OrgData } from "@/utils/orgs";
|
import { AccessCode, isAdmin, isCrawler, type OrgData } from "@/utils/orgs";
|
||||||
|
import slugifyStrict from "@/utils/slugify";
|
||||||
import appState, { AppStateService, use } from "@/utils/state";
|
import appState, { AppStateService, use } from "@/utils/state";
|
||||||
import { formatAPIUser } from "@/utils/user";
|
import { formatAPIUser } from "@/utils/user";
|
||||||
|
|
||||||
@ -233,7 +233,7 @@ export class OrgSettings extends TailwindElement {
|
|||||||
window.location.hostname
|
window.location.hostname
|
||||||
}/orgs/${
|
}/orgs/${
|
||||||
this.slugValue
|
this.slugValue
|
||||||
? this.slugify(this.slugValue)
|
? slugifyStrict(this.slugValue)
|
||||||
: this.org.slug
|
: this.org.slug
|
||||||
}`,
|
}`,
|
||||||
)}
|
)}
|
||||||
@ -454,12 +454,6 @@ export class OrgSettings extends TailwindElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private slugify(value: string) {
|
|
||||||
return slugify(value, {
|
|
||||||
strict: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkFormValidity(formEl: HTMLFormElement) {
|
private async checkFormValidity(formEl: HTMLFormElement) {
|
||||||
await this.updateComplete;
|
await this.updateComplete;
|
||||||
return !formEl.querySelector("[data-invalid]");
|
return !formEl.querySelector("[data-invalid]");
|
||||||
@ -502,7 +496,7 @@ export class OrgSettings extends TailwindElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (this.slugValue) {
|
if (this.slugValue) {
|
||||||
params.slug = this.slugify(this.slugValue);
|
params.slug = slugifyStrict(this.slugValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isSavingOrgName = true;
|
this.isSavingOrgName = true;
|
||||||
@ -634,10 +628,12 @@ export class OrgSettings extends TailwindElement {
|
|||||||
if (e.details === "duplicate_org_name") {
|
if (e.details === "duplicate_org_name") {
|
||||||
message = msg("This org name is already taken, try another one.");
|
message = msg("This org name is already taken, try another one.");
|
||||||
} else if (e.details === "duplicate_org_slug") {
|
} else if (e.details === "duplicate_org_slug") {
|
||||||
message = msg("This org URL is already taken, try another one.");
|
message = msg(
|
||||||
|
"This org URL identifier is already taken, try another one.",
|
||||||
|
);
|
||||||
} else if (e.details === "invalid_slug") {
|
} else if (e.details === "invalid_slug") {
|
||||||
message = msg(
|
message = msg(
|
||||||
"This org URL is invalid. Please use alphanumeric characters and dashes (-) only.",
|
"This org URL identifier is invalid. Please use alphanumeric characters and dashes (-) only.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -166,14 +166,15 @@
|
|||||||
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-100);
|
box-shadow: 0 0 0 var(--sl-focus-ring-width) var(--sl-color-danger-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-user-invalid]:not([disabled])::part(form-control-label):after {
|
[data-user-invalid]:not([disabled])::part(form-control-label),
|
||||||
/* Required asterisk color */
|
/* Required asterisk color */
|
||||||
color: var(--sl-color-danger-500);
|
[data-user-invalid]:not([disabled])::part(form-control-label)::after {
|
||||||
|
color: var(--sl-color-danger-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-user-invalid]:not([disabled])::part(form-control-help-text),
|
[data-user-invalid]:not([disabled])::part(form-control-help-text),
|
||||||
[data-user-invalid]:not([disabled]) .form-help-text {
|
[data-user-invalid]:not([disabled]) .form-help-text {
|
||||||
color: var(--sl-color-danger-500);
|
color: var(--sl-color-danger-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* TODO tailwind sets border-width: 0, see if this can be fixed in tw */
|
/* TODO tailwind sets border-width: 0, see if this can be fixed in tw */
|
||||||
|
|||||||
@ -39,17 +39,27 @@ export function getHelpText(maxLength: number, currentLength: number) {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function maxLengthValidator(maxLength: number): MaxLengthValidator {
|
export function maxLengthValidator(maxLength: number): MaxLengthValidator {
|
||||||
const helpText = msg(str`Maximum ${maxLength} characters`);
|
const validityHelpText = msg(str`Maximum ${maxLength} characters`);
|
||||||
|
let origHelpText: null | string = null;
|
||||||
|
|
||||||
const validate = (e: CustomEvent) => {
|
const validate = (e: CustomEvent) => {
|
||||||
const el = e.target as SlTextarea | SlInput;
|
const el = e.target as SlTextarea | SlInput;
|
||||||
const helpText = getHelpText(maxLength, el.value.length);
|
|
||||||
|
if (origHelpText === null && el.helpText) {
|
||||||
|
origHelpText = el.helpText;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validityText = getHelpText(maxLength, el.value.length);
|
||||||
|
const isInvalid = el.value.length > maxLength;
|
||||||
|
|
||||||
el.setCustomValidity(
|
el.setCustomValidity(
|
||||||
el.value.length > maxLength
|
isInvalid
|
||||||
? msg(str`Please shorten this text to ${maxLength} or less characters.`)
|
? msg(str`Please shorten this text to ${maxLength} or fewer characters.`)
|
||||||
: "",
|
: "",
|
||||||
);
|
);
|
||||||
el.helpText = helpText;
|
|
||||||
|
el.helpText = isInvalid ? validityText : origHelpText || validityHelpText;
|
||||||
};
|
};
|
||||||
|
|
||||||
return { helpText, validate };
|
return { helpText: validityHelpText, validate };
|
||||||
}
|
}
|
||||||
|
|||||||
7
frontend/src/utils/slugify.ts
Normal file
7
frontend/src/utils/slugify.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import slugify from "slugify";
|
||||||
|
|
||||||
|
import { getLocale } from "./localization";
|
||||||
|
|
||||||
|
export default function slugifyStrict(value: string) {
|
||||||
|
return slugify(value, { strict: true, lower: true, locale: getLocale() });
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user