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",
|
"pretty-ms": "^7.0.1",
|
||||||
"query-string": "^8.1.0",
|
"query-string": "^8.1.0",
|
||||||
"regex-colorize": "^0.0.3",
|
"regex-colorize": "^0.0.3",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"style-loader": "^3.3.0",
|
"style-loader": "^3.3.0",
|
||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"ts-loader": "^9.2.6",
|
"ts-loader": "^9.2.6",
|
||||||
|
@ -31,7 +31,7 @@ import "./components/new-collection-dialog";
|
|||||||
import "./components/new-workflow-dialog";
|
import "./components/new-workflow-dialog";
|
||||||
import type {
|
import type {
|
||||||
Member,
|
Member,
|
||||||
OrgNameChangeEvent,
|
OrgInfoChangeEvent,
|
||||||
UserRoleChangeEvent,
|
UserRoleChangeEvent,
|
||||||
OrgRemoveMemberEvent,
|
OrgRemoveMemberEvent,
|
||||||
} from "./settings";
|
} from "./settings";
|
||||||
@ -104,7 +104,7 @@ export class Org extends LiteElement {
|
|||||||
private org?: OrgData | null;
|
private org?: OrgData | null;
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private isSavingOrgName = false;
|
private isSavingOrgInfo = false;
|
||||||
|
|
||||||
get userOrg() {
|
get userOrg() {
|
||||||
if (!this.userInfo) return null;
|
if (!this.userInfo) return null;
|
||||||
@ -535,8 +535,8 @@ export class Org extends LiteElement {
|
|||||||
.orgId=${this.orgId}
|
.orgId=${this.orgId}
|
||||||
activePanel=${activePanel}
|
activePanel=${activePanel}
|
||||||
?isAddingMember=${isAddingMember}
|
?isAddingMember=${isAddingMember}
|
||||||
?isSavingOrgName=${this.isSavingOrgName}
|
?isSavingOrgName=${this.isSavingOrgInfo}
|
||||||
@org-name-change=${this.onOrgNameChange}
|
@org-info-change=${this.onOrgInfoChange}
|
||||||
@org-user-role-change=${this.onUserRoleChange}
|
@org-user-role-change=${this.onUserRoleChange}
|
||||||
@org-remove-member=${this.onOrgRemoveMember}
|
@org-remove-member=${this.onOrgRemoveMember}
|
||||||
></btrix-org-settings>`;
|
></btrix-org-settings>`;
|
||||||
@ -554,18 +554,17 @@ export class Org extends LiteElement {
|
|||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
private async onOrgInfoChange(e: OrgInfoChangeEvent) {
|
||||||
private async onOrgNameChange(e: OrgNameChangeEvent) {
|
this.isSavingOrgInfo = true;
|
||||||
this.isSavingOrgName = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apiFetch(`/orgs/${this.org!.id}/rename`, this.authState!, {
|
await this.apiFetch(`/orgs/${this.org!.id}/rename`, this.authState!, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ name: e.detail.value }),
|
body: JSON.stringify(e.detail),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.notify({
|
this.notify({
|
||||||
message: msg("Updated organization name."),
|
message: msg("Updated organization."),
|
||||||
variant: "success",
|
variant: "success",
|
||||||
icon: "check2-circle",
|
icon: "check2-circle",
|
||||||
});
|
});
|
||||||
@ -577,13 +576,13 @@ export class Org extends LiteElement {
|
|||||||
this.notify({
|
this.notify({
|
||||||
message: e.isApiError
|
message: e.isApiError
|
||||||
? e.message
|
? e.message
|
||||||
: msg("Sorry, couldn't update organization name at this time."),
|
: msg("Sorry, couldn't update organization at this time."),
|
||||||
variant: "danger",
|
variant: "danger",
|
||||||
icon: "exclamation-octagon",
|
icon: "exclamation-octagon",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isSavingOrgName = false;
|
this.isSavingOrgInfo = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onOrgRemoveMember(e: OrgRemoveMemberEvent) {
|
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 { msg, localized, str } from "@lit/localize";
|
||||||
import { when } from "lit/directives/when.js";
|
import { when } from "lit/directives/when.js";
|
||||||
import { serialize } from "@shoelace-style/shoelace/dist/utilities/form.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 type { AuthState } from "../../utils/AuthService";
|
||||||
import LiteElement, { html } from "../../utils/LiteElement";
|
import LiteElement, { html } from "../../utils/LiteElement";
|
||||||
@ -24,8 +26,9 @@ type Invite = User & {
|
|||||||
export type Member = User & {
|
export type Member = User & {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
export type OrgNameChangeEvent = CustomEvent<{
|
export type OrgInfoChangeEvent = CustomEvent<{
|
||||||
value: string;
|
name: string;
|
||||||
|
slug?: string;
|
||||||
}>;
|
}>;
|
||||||
export type UserRoleChangeEvent = CustomEvent<{
|
export type UserRoleChangeEvent = CustomEvent<{
|
||||||
user: Member;
|
user: Member;
|
||||||
@ -48,7 +51,7 @@ export type OrgRemoveMemberEvent = CustomEvent<{
|
|||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @events
|
* @events
|
||||||
* org-name-change
|
* org-info-change
|
||||||
* org-user-role-change
|
* org-user-role-change
|
||||||
* org-remove-member
|
* org-remove-member
|
||||||
*/
|
*/
|
||||||
@ -84,14 +87,17 @@ export class OrgSettings extends LiteElement {
|
|||||||
@state()
|
@state()
|
||||||
private isSubmittingInvite = false;
|
private isSubmittingInvite = false;
|
||||||
|
|
||||||
private get tabLabels() {
|
@state()
|
||||||
|
private slugValue = "";
|
||||||
|
|
||||||
|
private get tabLabels(): Record<Tab, string> {
|
||||||
return {
|
return {
|
||||||
information: msg("Org Information"),
|
information: msg("General"),
|
||||||
members: msg("Members"),
|
members: msg("Members"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private validateOrgNameMax = maxLengthValidator(50);
|
private validateOrgNameMax = maxLengthValidator(40);
|
||||||
|
|
||||||
async willUpdate(changedProperties: Map<string, any>) {
|
async willUpdate(changedProperties: Map<string, any>) {
|
||||||
if (changedProperties.has("isAddingMember") && this.isAddingMember) {
|
if (changedProperties.has("isAddingMember") && this.isAddingMember) {
|
||||||
@ -164,28 +170,80 @@ export class OrgSettings extends LiteElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private renderInformation() {
|
private renderInformation() {
|
||||||
return html`<div class="rounded border p-5">
|
return html`<div class="rounded border">
|
||||||
<form class="inline-control-form" @submit=${this.onOrgNameSubmit}>
|
<form @submit=${this.onOrgInfoSubmit}>
|
||||||
<sl-input
|
<div class="grid grid-cols-5 gap-x-4 p-4">
|
||||||
class="inline-control-input with-max-help-text"
|
<div class="col-span-5 md:col-span-3">
|
||||||
name="orgName"
|
<sl-input
|
||||||
size="small"
|
class="with-max-help-text"
|
||||||
label=${msg("Org Name")}
|
name="orgName"
|
||||||
autocomplete="off"
|
size="small"
|
||||||
value=${this.org.name}
|
label=${msg("Org Name")}
|
||||||
required
|
placeholder=${msg("My Organization")}
|
||||||
help-text=${this.validateOrgNameMax.helpText}
|
autocomplete="off"
|
||||||
@sl-input=${this.validateOrgNameMax.validate}
|
value=${this.org.name}
|
||||||
></sl-input>
|
minlength="2"
|
||||||
<sl-button
|
required
|
||||||
class="inline-control-button"
|
help-text=${this.validateOrgNameMax.helpText}
|
||||||
type="submit"
|
@sl-input=${this.validateOrgNameMax.validate}
|
||||||
size="small"
|
></sl-input>
|
||||||
variant="primary"
|
</div>
|
||||||
?disabled=${this.isSavingOrgName}
|
<div class="col-span-5 md:col-span-2 flex gap-2 pt-6">
|
||||||
?loading=${this.isSavingOrgName}
|
<div class="text-base">
|
||||||
>${msg("Save Changes")}</sl-button
|
<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>
|
</form>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@ -362,6 +420,12 @@ export class OrgSettings extends LiteElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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]");
|
||||||
@ -390,16 +454,23 @@ export class OrgSettings extends LiteElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onOrgNameSubmit(e: SubmitEvent) {
|
private async onOrgInfoSubmit(e: SubmitEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const formEl = e.target as HTMLFormElement;
|
const formEl = e.target as HTMLFormElement;
|
||||||
if (!(await this.checkFormValidity(formEl))) return;
|
if (!(await this.checkFormValidity(formEl))) return;
|
||||||
|
|
||||||
const { orgName } = serialize(formEl);
|
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(
|
this.dispatchEvent(
|
||||||
<OrgNameChangeEvent>new CustomEvent("org-name-change", {
|
<OrgInfoChangeEvent>new CustomEvent("org-info-change", {
|
||||||
detail: { value: orgName },
|
detail,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ export const AccessCode: Record<UserRole, number> = {
|
|||||||
export type OrgData = {
|
export type OrgData = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
slug: string;
|
||||||
quotas: Record<string, number>;
|
quotas: Record<string, number>;
|
||||||
bytesStored: number;
|
bytesStored: number;
|
||||||
users?: {
|
users?: {
|
||||||
|
@ -6191,6 +6191,11 @@ slice-ansi@^5.0.0:
|
|||||||
ansi-styles "^6.0.0"
|
ansi-styles "^6.0.0"
|
||||||
is-fullwidth-code-point "^4.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:
|
sockjs@^0.3.24:
|
||||||
version "0.3.24"
|
version "0.3.24"
|
||||||
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
|
resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce"
|
||||||
|
Loading…
Reference in New Issue
Block a user