Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: convert Dropdown components to TS #3608

Merged
merged 15 commits into from
Nov 27, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions framework/core/js/src/admin/AdminApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface AdminApplicationData extends ApplicationData {
modelStatistics: Record<string, { total: number }>;
displayNameDrivers: string[];
slugDrivers: Record<string, string[]>;
permissions: Record<string, string[]>;
}

export default class AdminApplication extends Application {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import app from '../../admin/app';
import Dropdown from '../../common/components/Dropdown';
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import Separator from '../../common/components/Separator';
import Group from '../../common/models/Group';
import Badge from '../../common/components/Badge';
import GroupBadge from '../../common/components/GroupBadge';
import Mithril from 'mithril';

function badgeForId(id) {
function badgeForId(id: string) {
const group = app.store.getById('groups', id);

return group ? GroupBadge.component({ group, label: null }) : '';
}

function filterByRequiredPermissions(groupIds, permission) {
function filterByRequiredPermissions(groupIds: string[], permission: string) {
app.getRequiredPermissions(permission).forEach((required) => {
const restrictToGroupIds = app.data.permissions[required] || [];

Expand All @@ -32,15 +33,19 @@ function filterByRequiredPermissions(groupIds, permission) {
return groupIds;
}

export default class PermissionDropdown extends Dropdown {
static initAttrs(attrs) {
export interface IPermissionDropdownAttrs extends IDropdownAttrs {
permission: string;
}

export default class PermissionDropdown<CustomAttrs extends IPermissionDropdownAttrs = IPermissionDropdownAttrs> extends Dropdown<CustomAttrs> {
static initAttrs(attrs: IPermissionDropdownAttrs) {
super.initAttrs(attrs);

attrs.className = 'PermissionDropdown';
attrs.buttonClassName = 'Button Button--text';
}

view(vnode) {
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
const children = [];

let groupIds = app.data.permissions[this.attrs.permission] || [];
Expand All @@ -49,7 +54,7 @@ export default class PermissionDropdown extends Dropdown {

const everyone = groupIds.indexOf(Group.GUEST_ID) !== -1;
const members = groupIds.indexOf(Group.MEMBER_ID) !== -1;
SychO9 marked this conversation as resolved.
Show resolved Hide resolved
const adminGroup = app.store.getById('groups', Group.ADMINISTRATOR_ID);
const adminGroup = app.store.getById<Group>('groups', Group.ADMINISTRATOR_ID)!;

if (everyone) {
this.attrs.label = Badge.component({ icon: 'fas fa-globe' });
Expand Down Expand Up @@ -89,40 +94,39 @@ export default class PermissionDropdown extends Dropdown {
{
icon: !everyone && !members ? 'fas fa-check' : true,
disabled: !everyone && !members,
onclick: (e) => {
onclick: (e: MouseEvent) => {
if (e.shiftKey) e.stopPropagation();
this.save([]);
},
},
[badgeForId(adminGroup.id()), ' ', adminGroup.namePlural()]
[badgeForId(adminGroup.id()!), ' ', adminGroup.namePlural()]
)
);

[].push.apply(
children,
app.store
.all('groups')
.filter((group) => [Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].indexOf(group.id()) === -1)
.map((group) =>
Button.component(
{
icon: groupIds.indexOf(group.id()) !== -1 ? 'fas fa-check' : true,
onclick: (e) => {
if (e.shiftKey) e.stopPropagation();
this.toggle(group.id());
},
disabled: this.isGroupDisabled(group.id()) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
const groupButtons = app.store
.all<Group>('groups')
.filter((group) => ![Group.ADMINISTRATOR_ID, Group.GUEST_ID, Group.MEMBER_ID].includes(group.id()!))
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we break this array out into a constant? Also, is it just me, or did we have a groups selector component at one point?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component itself is the selector you're referring to I believe. It doesn't just list groups though, but different times of items depending on the permission.

.map((group) =>
Button.component(
davwheat marked this conversation as resolved.
Show resolved Hide resolved
{
icon: groupIds.includes(group.id()!) ? 'fas fa-check' : true,
onclick: (e: MouseEvent) => {
if (e.shiftKey) e.stopPropagation();
this.toggle(group.id()!);
},
[badgeForId(group.id()), ' ', group.namePlural()]
)
disabled: this.isGroupDisabled(group.id()!) && this.isGroupDisabled(Group.MEMBER_ID) && this.isGroupDisabled(Group.GUEST_ID),
},
[badgeForId(group.id()!), ' ', group.namePlural()]
)
);
);

children.push(...groupButtons);
}

return super.view({ ...vnode, children });
}

save(groupIds) {
save(groupIds: string[]) {
const permission = this.attrs.permission;

app.data.permissions[permission] = groupIds;
Expand All @@ -134,7 +138,7 @@ export default class PermissionDropdown extends Dropdown {
});
}

toggle(groupId) {
toggle(groupId: string) {
const permission = this.attrs.permission;

let groupIds = app.data.permissions[permission] || [];
Expand All @@ -151,7 +155,7 @@ export default class PermissionDropdown extends Dropdown {
this.save(groupIds);
}

isGroupDisabled(id) {
return filterByRequiredPermissions([id], this.attrs.permission).indexOf(id) === -1;
isGroupDisabled(id: string) {
return !filterByRequiredPermissions([id], this.attrs.permission).includes(id);
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
import app from '../../admin/app';
import avatar from '../../common/helpers/avatar';
import username from '../../common/helpers/username';
import Dropdown from '../../common/components/Dropdown';
import Dropdown, { IDropdownAttrs } from '../../common/components/Dropdown';
import Button from '../../common/components/Button';
import ItemList from '../../common/utils/ItemList';
import type Mithril from 'mithril';

export interface ISessionDropdownAttrs extends IDropdownAttrs {}

/**
* The `SessionDropdown` component shows a button with the current user's
* avatar/name, with a dropdown of session controls.
*/
export default class SessionDropdown extends Dropdown {
static initAttrs(attrs) {
export default class SessionDropdown<CustomAttrs extends ISessionDropdownAttrs = ISessionDropdownAttrs> extends Dropdown<CustomAttrs> {
static initAttrs(attrs: ISessionDropdownAttrs) {
super.initAttrs(attrs);

attrs.className = 'SessionDropdown';
attrs.buttonClassName = 'Button Button--user Button--flat';
attrs.menuClassName = 'Dropdown-menu--right';
}

view(vnode) {
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
return super.view({ ...vnode, children: this.items().toArray() });
}

Expand All @@ -30,11 +33,9 @@ export default class SessionDropdown extends Dropdown {

/**
* Build an item list for the contents of the dropdown menu.
*
* @return {ItemList}
*/
items() {
const items = new ItemList();
items(): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();

items.add(
'logOut',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import app from '../app';
import SelectDropdown from '../../common/components/SelectDropdown';
import SelectDropdown, { ISelectDropdownAttrs } from '../../common/components/SelectDropdown';
import Button from '../../common/components/Button';
import saveSettings from '../utils/saveSettings';
import Mithril from 'mithril';

export default class SettingDropdown extends SelectDropdown {
static initAttrs(attrs) {
export type SettingDropdownOption = {
value: any;
label: string;
};

export interface ISettingDropdownAttrs extends ISelectDropdownAttrs {
setting?: string;
options: Array<SettingDropdownOption>;
}

export default class SettingDropdown<CustomAttrs extends ISettingDropdownAttrs = ISettingDropdownAttrs> extends SelectDropdown<CustomAttrs> {
static initAttrs(attrs: ISettingDropdownAttrs) {
super.initAttrs(attrs);

attrs.className = 'SettingDropdown';
Expand All @@ -13,21 +24,21 @@ export default class SettingDropdown extends SelectDropdown {
attrs.defaultLabel = 'Custom';

if ('key' in attrs) {
attrs.setting = attrs.key;
attrs.setting = attrs.key?.toString();
delete attrs.key;
}
}

view(vnode) {
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
return super.view({
...vnode,
children: this.attrs.options.map(({ value, label }) => {
const active = app.data.settings[this.attrs.setting] === value;
const active = app.data.settings[this.attrs.setting!] === value;

return Button.component(
{
icon: active ? 'fas fa-check' : true,
onclick: saveSettings.bind(this, { [this.attrs.setting]: value }),
onclick: saveSettings.bind(this, { [this.attrs.setting!]: value }),
active,
},
label
Expand Down
Original file line number Diff line number Diff line change
@@ -1,54 +1,62 @@
import app from '../../common/app';
import Component from '../Component';
import Component, { ComponentAttrs } from '../Component';
import icon from '../helpers/icon';
import listItems from '../helpers/listItems';
import listItems, { ModdedChildrenWithItemName } from '../helpers/listItems';
import extractText from '../utils/extractText';
import type Mithril from 'mithril';

export interface IDropdownAttrs extends ComponentAttrs {
/** A class name to apply to the dropdown toggle button. */
buttonClassName?: string;
/** A class name to apply to the dropdown menu. */
menuClassName?: string;
/** The name of an icon to show in the dropdown toggle button. */
icon?: string;
/** The name of an icon to show on the right of the button. */
caretIcon?: string;
/** The label of the dropdown toggle button. Defaults to 'Controls'. */
label: Mithril.Children;
/** The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'. */
accessibleToggleLabel?: string;
/** An action to take when the dropdown is collapsed. */
onhide?: () => void;
/** An action to take when the dropdown is opened. */
onshow?: () => void;

lazyDraw?: boolean;
}

/**
* The `Dropdown` component displays a button which, when clicked, shows a
* dropdown menu beneath it.
*
* ### Attrs
*
* - `buttonClassName` A class name to apply to the dropdown toggle button.
* - `menuClassName` A class name to apply to the dropdown menu.
* - `icon` The name of an icon to show in the dropdown toggle button.
* - `caretIcon` The name of an icon to show on the right of the button.
* - `label` The label of the dropdown toggle button. Defaults to 'Controls'.
* - `accessibleToggleLabel` The label used to describe the dropdown toggle button to assistive readers. Defaults to 'Toggle dropdown menu'.
* - `onhide`
* - `onshow`
*
* The children will be displayed as a list inside of the dropdown menu.
* The children will be displayed as a list inside the dropdown menu.
*/
export default class Dropdown extends Component {
static initAttrs(attrs) {
export default class Dropdown<CustomAttrs extends IDropdownAttrs = IDropdownAttrs> extends Component<CustomAttrs> {
protected showing = false;

static initAttrs(attrs: IDropdownAttrs) {
attrs.className = attrs.className || '';
attrs.buttonClassName = attrs.buttonClassName || '';
attrs.menuClassName = attrs.menuClassName || '';
attrs.label = attrs.label || '';
SychO9 marked this conversation as resolved.
Show resolved Hide resolved
attrs.caretIcon = typeof attrs.caretIcon !== 'undefined' ? attrs.caretIcon : 'fas fa-caret-down';
SychO9 marked this conversation as resolved.
Show resolved Hide resolved
attrs.accessibleToggleLabel = attrs.accessibleToggleLabel || app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label');
attrs.accessibleToggleLabel ||= extractText(app.translator.trans('core.lib.dropdown.toggle_dropdown_accessible_label'));
}

oninit(vnode) {
super.oninit(vnode);

this.showing = false;
}

view(vnode) {
const items = vnode.children ? listItems(vnode.children) : [];
view(vnode: Mithril.Vnode<CustomAttrs, this>) {
const items = vnode.children ? listItems(vnode.children as ModdedChildrenWithItemName[]) : [];
const renderItems = this.attrs.lazyDraw ? this.showing : true;

return (
<div className={'ButtonGroup Dropdown dropdown ' + this.attrs.className + ' itemCount' + items.length + (this.showing ? ' open' : '')}>
{this.getButton(vnode.children)}
{this.getButton(vnode.children as Mithril.ChildArray)}
{renderItems && this.getMenu(items)}
</div>
);
}

oncreate(vnode) {
oncreate(vnode: Mithril.VnodeDOM<CustomAttrs, this>) {
super.oncreate(vnode);

// When opening the dropdown menu, work out if the menu goes beyond the
Expand Down Expand Up @@ -78,15 +86,25 @@ export default class Dropdown extends Component {
const $menu = this.$('.Dropdown-menu');
const isRight = $menu.hasClass('Dropdown-menu--right');

const top = $menu.offset()?.top || 0;
const height = $menu.height() || 0;
const windowSrollTop = $(window).scrollTop() || 0;
const windowHeight = $(window).height() || 0;

$menu.removeClass('Dropdown-menu--top Dropdown-menu--right');

$menu.toggleClass('Dropdown-menu--top', $menu.offset().top + $menu.height() > $(window).scrollTop() + $(window).height());
$menu.toggleClass('Dropdown-menu--top', top + height > windowSrollTop + windowHeight);

if ($menu.offset().top < 0) {
if (($menu.offset()?.top || 0) < 0) {
$menu.removeClass('Dropdown-menu--top');
}

$menu.toggleClass('Dropdown-menu--right', isRight || $menu.offset().left + $menu.width() > $(window).scrollLeft() + $(window).width());
const left = $menu.offset()?.left || 0;
const width = $menu.width() || 0;
const windowScrollLeft = $(window).scrollLeft() || 0;
const windowWidth = $(window).width() || 0;

SychO9 marked this conversation as resolved.
Show resolved Hide resolved
$menu.toggleClass('Dropdown-menu--right', isRight || left + width > windowScrollLeft + windowWidth);
});

this.$().on('hidden.bs.dropdown', () => {
Expand All @@ -102,11 +120,8 @@ export default class Dropdown extends Component {

/**
* Get the template for the button.
*
* @return {import('mithril').Children}
* @protected
*/
getButton(children) {
getButton(children: Mithril.ChildArray): Mithril.Vnode<any, any> {
return (
<button
className={'Dropdown-toggle ' + this.attrs.buttonClassName}
Expand All @@ -122,19 +137,16 @@ export default class Dropdown extends Component {

/**
* Get the template for the button's content.
*
* @return {import('mithril').Children}
* @protected
*/
getButtonContent(children) {
getButtonContent(children: Mithril.ChildArray): Mithril.ChildArray {
return [
this.attrs.icon ? icon(this.attrs.icon, { className: 'Button-icon' }) : '',
<span className="Button-label">{this.attrs.label}</span>,
this.attrs.caretIcon ? icon(this.attrs.caretIcon, { className: 'Button-caret' }) : '',
];
}

getMenu(items) {
getMenu(items: Mithril.Vnode<any, any>[]): Mithril.Vnode<any, any> {
return <ul className={'Dropdown-menu dropdown-menu ' + this.attrs.menuClassName}>{items}</ul>;
}
}
Loading