Allow org admins to update slug (#1276)

- Allows editing of org slugs (actual URL updates will be handled in
https://github.com/webrecorder/browsertrix-cloud/issues/1258.)
- Converts user input to slug using slugify
- Adds help text to org name and slug
- Renames tab from "information" to "general" settings
This commit is contained in:
sua yoo 2023-10-13 17:00:43 -07:00 committed by GitHub
parent 0bd8748e68
commit 8466caf1d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 119 additions and 42 deletions

View File

@ -50,6 +50,7 @@
"pretty-ms": "^7.0.1",
"query-string": "^8.1.0",
"regex-colorize": "^0.0.3",
"slugify": "^1.6.6",
"style-loader": "^3.3.0",
"tailwindcss": "^3.2.7",
"ts-loader": "^9.2.6",

View File

@ -31,7 +31,7 @@ import "./components/new-collection-dialog";
import "./components/new-workflow-dialog";
import type {
Member,
OrgNameChangeEvent,
OrgInfoChangeEvent,
UserRoleChangeEvent,
OrgRemoveMemberEvent,
} from "./settings";
@ -104,7 +104,7 @@ export class Org extends LiteElement {
private org?: OrgData | null;
@state()
private isSavingOrgName = false;
private isSavingOrgInfo = false;
get userOrg() {
if (!this.userInfo) return null;
@ -535,8 +535,8 @@ export class Org extends LiteElement {
.orgId=${this.orgId}
activePanel=${activePanel}
?isAddingMember=${isAddingMember}
?isSavingOrgName=${this.isSavingOrgName}
@org-name-change=${this.onOrgNameChange}
?isSavingOrgName=${this.isSavingOrgInfo}
@org-info-change=${this.onOrgInfoChange}
@org-user-role-change=${this.onUserRoleChange}
@org-remove-member=${this.onOrgRemoveMember}
></btrix-org-settings>`;
@ -554,18 +554,17 @@ export class Org extends LiteElement {
return data;
}
private async onOrgNameChange(e: OrgNameChangeEvent) {
this.isSavingOrgName = true;
private async onOrgInfoChange(e: OrgInfoChangeEvent) {
this.isSavingOrgInfo = true;
try {
await this.apiFetch(`/orgs/${this.org!.id}/rename`, this.authState!, {
method: "POST",
body: JSON.stringify({ name: e.detail.value }),
body: JSON.stringify(e.detail),
});
this.notify({
message: msg("Updated organization name."),
message: msg("Updated organization."),
variant: "success",
icon: "check2-circle",
});
@ -577,13 +576,13 @@ export class Org extends LiteElement {
this.notify({
message: e.isApiError
? e.message
: msg("Sorry, couldn't update organization name at this time."),
: msg("Sorry, couldn't update organization at this time."),
variant: "danger",
icon: "exclamation-octagon",
});
}
this.isSavingOrgName = false;
this.isSavingOrgInfo = false;
}
private async onOrgRemoveMember(e: OrgRemoveMemberEvent) {

View File

@ -3,6 +3,8 @@ import { ifDefined } from "lit/directives/if-defined.js";
import { msg, localized, str } from "@lit/localize";
import { when } from "lit/directives/when.js";
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.js";
import slugify from "slugify";
import type { SlInput } from "@shoelace-style/shoelace";
import type { AuthState } from "../../utils/AuthService";
import LiteElement, { html } from "../../utils/LiteElement";
@ -24,8 +26,9 @@ type Invite = User & {
export type Member = User & {
name: string;
};
export type OrgNameChangeEvent = CustomEvent<{
value: string;
export type OrgInfoChangeEvent = CustomEvent<{
name: string;
slug?: string;
}>;
export type UserRoleChangeEvent = CustomEvent<{
user: Member;
@ -48,7 +51,7 @@ export type OrgRemoveMemberEvent = CustomEvent<{
* ```
*
* @events
* org-name-change
* org-info-change
* org-user-role-change
* org-remove-member
*/
@ -84,14 +87,17 @@ export class OrgSettings extends LiteElement {
@state()
private isSubmittingInvite = false;
private get tabLabels() {
@state()
private slugValue = "";
private get tabLabels(): Record<Tab, string> {
return {
information: msg("Org Information"),
information: msg("General"),
members: msg("Members"),
};
}
private validateOrgNameMax = maxLengthValidator(50);
private validateOrgNameMax = maxLengthValidator(40);
async willUpdate(changedProperties: Map<string, any>) {
if (changedProperties.has("isAddingMember") && this.isAddingMember) {
@ -164,28 +170,80 @@ export class OrgSettings extends LiteElement {
}
private renderInformation() {
return html`<div class="rounded border p-5">
<form class="inline-control-form" @submit=${this.onOrgNameSubmit}>
<sl-input
class="inline-control-input with-max-help-text"
name="orgName"
size="small"
label=${msg("Org Name")}
autocomplete="off"
value=${this.org.name}
required
help-text=${this.validateOrgNameMax.helpText}
@sl-input=${this.validateOrgNameMax.validate}
></sl-input>
<sl-button
class="inline-control-button"
type="submit"
size="small"
variant="primary"
?disabled=${this.isSavingOrgName}
?loading=${this.isSavingOrgName}
>${msg("Save Changes")}</sl-button
>
return html`<div class="rounded border">
<form @submit=${this.onOrgInfoSubmit}>
<div class="grid grid-cols-5 gap-x-4 p-4">
<div class="col-span-5 md:col-span-3">
<sl-input
class="with-max-help-text"
name="orgName"
size="small"
label=${msg("Org Name")}
placeholder=${msg("My Organization")}
autocomplete="off"
value=${this.org.name}
minlength="2"
required
help-text=${this.validateOrgNameMax.helpText}
@sl-input=${this.validateOrgNameMax.validate}
></sl-input>
</div>
<div class="col-span-5 md:col-span-2 flex gap-2 pt-6">
<div class="text-base">
<sl-icon name="info-circle"></sl-icon>
</div>
<div class="mt-0.5 text-xs text-neutral-500">
${msg(
"Name of your organization that is visible to all org members."
)}
</div>
</div>
<div class="col-span-5 md:col-span-3">
<sl-input
name="orgSlug"
size="small"
label=${msg("Org ID")}
placeholder="my-organization"
autocomplete="off"
value=${this.org.slug}
minlength="2"
maxlength="30"
required
help-text=${msg(
str`Org URL will be ${window.location.protocol}//${
window.location.hostname
}/${
this.slugValue ? this.slugify(this.slugValue) : this.org.slug
}`
)}
@sl-input=${(e: InputEvent) => {
const input = e.target as SlInput;
this.slugValue = input.value;
}}
>
</sl-input>
</div>
<div class="col-span-5 md:col-span-2 flex gap-2 pt-6">
<div class="text-base">
<sl-icon name="info-circle"></sl-icon>
</div>
<div class="mt-0.5 text-xs text-neutral-500">
${msg("Unique URL for this organization.")}
</div>
</div>
</div>
<footer class="border-t flex justify-end px-4 py-3">
<sl-button
class="inline-control-button"
type="submit"
size="small"
variant="primary"
?disabled=${this.isSavingOrgName}
?loading=${this.isSavingOrgName}
>${msg("Save Changes")}</sl-button
>
</footer>
</form>
</div>`;
}
@ -362,6 +420,12 @@ export class OrgSettings extends LiteElement {
`;
}
private slugify(value: string) {
return slugify(value, {
strict: true,
});
}
private async checkFormValidity(formEl: HTMLFormElement) {
await this.updateComplete;
return !formEl.querySelector("[data-invalid]");
@ -390,16 +454,23 @@ export class OrgSettings extends LiteElement {
}
}
private async onOrgNameSubmit(e: SubmitEvent) {
private async onOrgInfoSubmit(e: SubmitEvent) {
e.preventDefault();
const formEl = e.target as HTMLFormElement;
if (!(await this.checkFormValidity(formEl))) return;
const { orgName } = serialize(formEl);
const orgSlug = this.slugify(this.slugValue);
const detail: any = { name: orgName };
if (orgSlug !== this.org.slug) {
detail.slug = orgSlug;
}
this.dispatchEvent(
<OrgNameChangeEvent>new CustomEvent("org-name-change", {
detail: { value: orgName },
<OrgInfoChangeEvent>new CustomEvent("org-info-change", {
detail,
})
);
}

View File

@ -11,6 +11,7 @@ export const AccessCode: Record<UserRole, number> = {
export type OrgData = {
id: string;
name: string;
slug: string;
quotas: Record<string, number>;
bytesStored: number;
users?: {

View File

@ -6191,6 +6191,11 @@ slice-ansi@^5.0.0:
ansi-styles "^6.0.0"
is-fullwidth-code-point "^4.0.0"
slugify@^1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.6.tgz#2d4ac0eacb47add6af9e04d3be79319cbcc7924b"
integrity sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==
sockjs@^0.3.24:
version "0.3.24"
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"