Skip to content

Commit

Permalink
EditableField types and tests (#240)
Browse files Browse the repository at this point in the history
* Fix CustomEditableField bug

* Update field.d.ts

* Update editable-field.hbs
  • Loading branch information
jeffdaley authored Jul 10, 2023
1 parent 1c4c5a7 commit ebee3e0
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 166 deletions.
4 changes: 2 additions & 2 deletions web/app/components/custom-editable-field.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<p
class="hds-typography-body-200 hds-foreground-primary truncate
{{unless documentField 'italic'}}"
title={{or documentField "None"}}
title="{{or documentField 'None'}}"
>
{{#if documentField}}
{{documentField}}
Expand All @@ -24,7 +24,7 @@
<:editing as |F|>
<Hds::Form::Textarea::Field
@value={{F.value}}
name={{field}}
name={{@field}}
{{auto-height-textarea}}
{{on "blur" F.update}}
data-test-custom-string-field-input
Expand Down
24 changes: 0 additions & 24 deletions web/app/components/custom-editable-field.js

This file was deleted.

51 changes: 51 additions & 0 deletions web/app/components/custom-editable-field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import {
CustomEditableField,
HermesDocument,
HermesUser,
} from "hermes/types/document";

interface CustomEditableFieldComponentSignature {
Args: {
document: HermesDocument;
field: string;
attributes: CustomEditableField;
onChange: (value: any) => void;
loading?: boolean;
disabled?: boolean;
};
}

export default class CustomEditableFieldComponent extends Component<CustomEditableFieldComponentSignature> {
@tracked protected emails: string | string[] =
this.args.attributes.value || [];

protected get typeIsString(): boolean {
return this.args.attributes.type === "STRING";
}

protected get typeIsPeople(): boolean {
return this.args.attributes.type === "PEOPLE";
}

protected get people(): HermesUser[] {
let emails = this.emails instanceof Array ? this.emails : [this.emails];
return emails.map((email: string) => {
return { email, imgURL: null };
});
}

@action protected updateEmails(people: HermesUser[]) {
this.emails = people.map((person: HermesUser) => {
return person.email;
});
}
}

declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
CustomEditableField: typeof CustomEditableFieldComponent;
}
}
1 change: 0 additions & 1 deletion web/app/components/document/sidebar.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,6 @@
@field={{field}}
@attributes={{attributes}}
@onChange={{perform this.save field}}
@updateFieldValue={{this.updateCustomFieldValue}}
@loading={{this.save.isRunning}}
@disabled={{or this.editingIsDisabled (not this.isOwner)}}
/>
Expand Down
13 changes: 0 additions & 13 deletions web/app/components/document/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,19 +330,6 @@ export default class DocumentSidebarComponent extends Component<DocumentSidebarC
this.contributors = contributors;
}

@action
updateCustomFieldValue(field: string, value: string) {
assert("customEditableFields must exist", this.customEditableFields);

const customEditableField = this.customEditableFields[field];
assert("customEditableField must exist", customEditableField);

let customEditableFieldValue = customEditableField.value;
assert("customEditableFieldValue must exist", customEditableFieldValue);

customEditableFieldValue = value;
}

@action closeDeleteModal() {
this.deleteModalIsActive = false;
}
Expand Down
72 changes: 50 additions & 22 deletions web/app/components/editable-field.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,92 @@
// @ts-nocheck
// TODO: Type this file
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { scheduleOnce } from "@ember/runloop";
import { assert } from "@ember/debug";

export const FOCUSABLE =
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

export default class EditableField extends Component {
@tracked editing = false;
@tracked element = null;
@tracked cachedValue = null;
interface EditableFieldComponentSignature {
Element: HTMLDivElement;
Args: {
value: any;
onChange: (value: any) => void;
loading?: boolean;
disabled?: boolean;
};
Blocks: {
default: [];
editing: [
F: {
value: any;
update: (value: any) => void;
}
];
};
}

export default class EditableFieldComponent extends Component<EditableFieldComponentSignature> {
@tracked protected editing = false;
@tracked protected el: HTMLElement | null = null;
@tracked protected cachedValue = null;

@action
captureElement(el) {
this.element = el;
@action protected captureElement(el: HTMLElement) {
this.el = el;
}

@action
edit() {
@action protected edit() {
this.cachedValue = this.args.value;
this.editing = true;

// Kinda gross, but this gives focus to the first focusable element in the
// :editing block, which will typically be an input.
scheduleOnce("afterRender", this, () => {
if (this.element && !this.element.contains(document.activeElement)) {
const firstInput = this.element.querySelector(FOCUSABLE);
if (firstInput) firstInput.focus();
if (this.el && !this.el.contains(document.activeElement)) {
const firstInput = this.el.querySelector(FOCUSABLE);
if (firstInput) (firstInput as HTMLElement).focus();
}
});
}

@action
cancel(ev) {
@action protected cancel(ev: KeyboardEvent) {
if (ev.key === "Escape") {
ev.preventDefault();
scheduleOnce("actions", this, () => {
this.editing = false;
});
ev.preventDefault();
}
}

@action
preventNewlines(ev) {
@action protected preventNewlines(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.preventDefault();
}
}

@action
update(ev) {
@action protected update(eventOrValue: Event | any) {
scheduleOnce("actions", this, () => {
this.editing = false;
});

const newValue = ev instanceof Event ? ev.target.value : ev;
let newValue = eventOrValue;

if (eventOrValue instanceof Event) {
const target = eventOrValue.target;
assert("target must exist", target);
assert("value must exist in the target", "value" in target);
const value = target.value;
newValue = value;
}

if (newValue !== this.cachedValue) {
this.args.onChange?.(newValue);
}
}
}

declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
EditableField: typeof EditableFieldComponent;
}
}
37 changes: 23 additions & 14 deletions web/app/components/inputs/people-select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ export interface GoogleUser {
photos: { url: string }[];
}

interface PeopleSelectComponentSignature {
interface InputsPeopleSelectComponentSignature {
Element: HTMLDivElement;
Args: {
selected: HermesUser[];
onBlur?: () => void;
onChange: (people: GoogleUser[]) => void;
onChange: (people: HermesUser[]) => void;
};
}

const MAX_RETRIES = 3;
const INITIAL_RETRY_DELAY = Ember.testing ? 0 : 500;

export default class PeopleSelectComponent extends Component<PeopleSelectComponentSignature> {
export default class InputsPeopleSelectComponent extends Component<InputsPeopleSelectComponentSignature> {
@service("fetch") declare fetchSvc: FetchService;

/**
Expand Down Expand Up @@ -76,17 +77,19 @@ export default class PeopleSelectComponent extends Component<PeopleSelectCompone
const peopleJson = await response?.json();

if (peopleJson) {
this.people = peopleJson.map((p: GoogleUser) => {
return {
email: p.emailAddresses[0]?.value,
imgURL: p.photos?.[0]?.url,
};
}).filter((person: HermesUser) => {
// filter out any people already selected
return !this.args.selected.find(
(selectedPerson) => selectedPerson.email === person.email
);
});
this.people = peopleJson
.map((p: GoogleUser) => {
return {
email: p.emailAddresses[0]?.value,
imgURL: p.photos?.[0]?.url,
};
})
.filter((person: HermesUser) => {
// filter out any people already selected
return !this.args.selected.find(
(selectedPerson) => selectedPerson.email === person.email
);
});
} else {
this.people = [];
}
Expand All @@ -108,3 +111,9 @@ export default class PeopleSelectComponent extends Component<PeopleSelectCompone
}
});
}

declare module "@glint/environment-ember-loose/registry" {
export default interface Registry {
"Inputs::PeopleSelect": typeof InputsPeopleSelectComponent;
}
}
2 changes: 1 addition & 1 deletion web/app/types/document.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface CustomEditableFields {
export interface CustomEditableField {
displayName: string;
type: "STRING" | "PEOPLE";
value?: string;
value?: string | string[];
}

export interface HermesUser {
Expand Down
Loading

0 comments on commit ebee3e0

Please sign in to comment.