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:
parent
0bd8748e68
commit
8466caf1d9
@ -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",
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -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?: {
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user