+ {{! div to break the parent's space-y styles }}
+
+
+ {{#if this.isDraft}}
+
+ <:anchor as |dd|>
+
+
+ <:item as |dd|>
+
+
+
+
{{dd.attrs.title}}
+
{{dd.attrs.description}}
+
+
+
+
+
+ {{/if}}
+
{{#if this.editingIsDisabled}}
{{this.title}}
{{else}}
-
- <:default>
- {{#unless (is-empty this.title)}}
- {{this.title}}
- {{else}}
- Enter a title here.
- {{/unless}}
-
- <:editing as |F|>
-
-
-
+
+
+ <:default>
+ {{#unless (is-empty this.title)}}
+ {{this.title}}
+ {{else}}
+ Enter a title here.
+ {{/unless}}
+
+ <:editing as |F|>
+
+
+
+
{{/if}}
-
+ {{! Summary }}
+
+
{{#if this.editingIsDisabled}}
-
+
+
+
+
diff --git a/web/app/components/document/sidebar.ts b/web/app/components/document/sidebar.ts
index 1595fec40..97e587ca8 100644
--- a/web/app/components/document/sidebar.ts
+++ b/web/app/components/document/sidebar.ts
@@ -3,8 +3,13 @@ import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { getOwner } from "@ember/application";
import { inject as service } from "@ember/service";
-import { restartableTask, task } from "ember-concurrency";
-import { dasherize } from "@ember/string";
+import {
+ keepLatestTask,
+ restartableTask,
+ task,
+ timeout,
+} from "ember-concurrency";
+import { capitalize, dasherize } from "@ember/string";
import cleanString from "hermes/utils/clean-string";
import { debounce } from "@ember/runloop";
import FetchService from "hermes/services/fetch";
@@ -15,6 +20,8 @@ import { AuthenticatedUser } from "hermes/services/authenticated-user";
import { HermesDocument, HermesUser } from "hermes/types/document";
import { assert } from "@ember/debug";
import Route from "@ember/routing/route";
+import Ember from "ember";
+import htmlElement from "hermes/utils/html-element";
interface DocumentSidebarComponentSignature {
Args: {
@@ -27,6 +34,24 @@ interface DocumentSidebarComponentSignature {
};
}
+export enum DraftVisibility {
+ Restricted = "restricted",
+ Shareable = "shareable",
+}
+
+export enum DraftVisibilityIcon {
+ Restricted = "lock",
+ Shareable = "enterprise",
+ Loading = "loading",
+}
+
+export enum DraftVisibilityDescription {
+ Restricted = "Only you and the people you add can view and edit this doc.",
+ Shareable = "Editing is restricted, but anyone in the organization with the link can view.",
+}
+
+const SHARE_BUTTON_SELECTOR = "#sidebar-header-copy-url-button";
+
export default class DocumentSidebarComponent extends Component {
@service("fetch") declare fetchSvc: FetchService;
@service declare router: RouterService;
@@ -64,9 +89,84 @@ export default class DocumentSidebarComponent extends Component {
+ /**
+ * A task that waits for a short time and then resolves.
+ * Used to trigger the "link created" state of the share button.
+ */
+ protected showCreateLinkSuccessMessage = restartableTask(async () => {
+ await timeout(Ember.testing ? 0 : 1000);
+ });
+
+ /**
+ * Sets the draft's `isShareable` property based on a selection
+ * in the draft-visibility dropdown. Immediately updates the UI
+ * to reflect the intended change while a request is made to the
+ * back end. Once the request completes, the UI is updated again
+ * to reflect the actual state of the document.
+ */
+ protected setDraftVisibility = restartableTask(
+ async (newVisibility: DraftVisibility) => {
+ if (this.draftVisibility === newVisibility) {
+ return;
+ }
+
+ try {
+ if (newVisibility === DraftVisibility.Restricted) {
+ this.newDraftVisibilityIcon = DraftVisibilityIcon.Restricted;
+
+ const shareButton = htmlElement(SHARE_BUTTON_SELECTOR);
+
+ shareButton.classList.add("out");
+
+ void this.fetchSvc.fetch(`/api/v1/drafts/${this.docID}/shareable`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ isShareable: false,
+ }),
+ });
+
+ // Give time for the link icon to animate out
+ await timeout(Ember.testing ? 0 : 300);
+
+ // With the animation done, we can now remove the button.
+ this._docIsShareable = false;
+ } else {
+ // Immediately update the UI to show the share button
+ // in its "creating link" state.
+ this.newDraftVisibilityIcon = DraftVisibilityIcon.Shareable;
+ this._docIsShareable = true;
+
+ await this.fetchSvc.fetch(`/api/v1/drafts/${this.docID}/shareable`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ isShareable: true,
+ }),
+ });
+
+ // Kick off the timer for the "link created" state.
+ void this.showCreateLinkSuccessMessage.perform();
+ }
+ } catch (error: unknown) {
+ this.showFlashError(
+ error as Error,
+ "Unable to update draft visibility"
+ );
+ } finally {
+ // reset the new-visibility-intent icon
+ this.newDraftVisibilityIcon = null;
+ }
+ }
+ );
+
+ updateProduct = keepLatestTask(async (product: string) => {
this.product = product;
await this.save.perform("product", this.product);
// productAbbreviation is computed by the back end
@@ -347,10 +576,33 @@ export default class DocumentSidebarComponent extends Component {
+ try {
+ const response = await this.fetchSvc
+ .fetch(`/api/v1/drafts/${this.docID}/shareable`)
+ .then((response) => response?.json());
+ if (response?.isShareable) {
+ this._docIsShareable = true;
+ }
+ } catch {}
+ });
+
approve = task(async () => {
try {
await this.fetchSvc.fetch(`/api/v1/approvals/${this.docID}`, {
diff --git a/web/app/components/document/sidebar/header.hbs b/web/app/components/document/sidebar/header.hbs
index 8f91ac1c3..3f550ef23 100644
--- a/web/app/components/document/sidebar/header.hbs
+++ b/web/app/components/document/sidebar/header.hbs
@@ -1,4 +1,3 @@
-{{! @glint-nocheck: not typesafe yet }}