diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index f161d7adb1..2b515f2412 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -700,6 +700,15 @@ export default class FNote { return this.hasAttribute(LABEL, name); } + /** + * Returns `true` if the note has a label with the given name (same as {@link hasOwnedLabel}), or it has a label with the `disabled:` prefix (for example due to a safe import). + * @param name the name of the label to look for. + * @returns `true` if the label exists, or its version with the `disabled:` prefix. + */ + hasLabelOrDisabled(name: string) { + return this.hasLabel(name) || this.hasLabel(`disabled:${name}`); + } + /** * @param name - label name * @returns true if label exists (including inherited) and does not have "false" value. diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index f8fb305726..cf88b7f2d2 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -210,6 +210,7 @@ --badge-share-background-color: #4d4d4d; --badge-clipped-note-background-color: #295773; --badge-execute-background-color: #604180; + --badge-active-content-background-color: rgb(12, 68, 70); --note-icon-background-color: #444444; --note-icon-color: #d4d4d4; @@ -238,9 +239,9 @@ --bottom-panel-background-color: #11111180; --bottom-panel-title-bar-background-color: #3F3F3F80; - + --status-bar-border-color: var(--main-border-color); - + --scrollbar-thumb-color: #fdfdfd5c; --scrollbar-thumb-hover-color: #ffffff7d; --scrollbar-background-color: transparent; @@ -351,4 +352,4 @@ body .todo-list input[type="checkbox"]:not(:checked):before { .note-split.with-hue *::selection, .quick-edit-dialog-wrapper.with-hue *::selection { --selection-background-color: hsl(var(--custom-color-hue), 49.2%, 35%); -} \ No newline at end of file +} diff --git a/apps/client/src/stylesheets/theme-next-light.css b/apps/client/src/stylesheets/theme-next-light.css index 2d7862ae00..00678c6ae9 100644 --- a/apps/client/src/stylesheets/theme-next-light.css +++ b/apps/client/src/stylesheets/theme-next-light.css @@ -202,6 +202,7 @@ --badge-share-background-color: #6b6b6b; --badge-clipped-note-background-color: #2284c0; --badge-execute-background-color: #7b47af; + --badge-active-content-background-color: rgb(27, 164, 168); --note-icon-background-color: #4f4f4f; --note-icon-color: white; @@ -322,4 +323,4 @@ .note-split.with-hue *::selection, .quick-edit-dialog-wrapper.with-hue *::selection { --selection-background-color: hsl(var(--custom-color-hue), 60%, 90%); -} \ No newline at end of file +} diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index f434ffeddd..3656035497 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2288,5 +2288,29 @@ }, "bookmark_buttons": { "bookmarks": "Bookmarks" + }, + "active_content_badges": { + "type_icon_pack": "Icon pack", + "type_backend_script": "Backend script", + "type_frontend_script": "Frontend script", + "type_widget": "Widget", + "type_app_css": "Custom CSS", + "type_render_note": "Render note", + "type_web_view": "Web view", + "type_app_theme": "Custom theme", + "toggle_tooltip_enable_tooltip": "Click to enable this {{type}}.", + "toggle_tooltip_disable_tooltip": "Click to disable this {{type}}.", + "menu_docs": "Open documentation", + "menu_execute_now": "Execute script now", + "menu_run": "Run automatically", + "menu_run_disabled": "Manually", + "menu_run_backend_startup": "When the backend starts up", + "menu_run_hourly": "Hourly", + "menu_run_daily": "Daily", + "menu_run_frontend_startup": "When the desktop frontend starts up", + "menu_run_mobile_startup": "When the mobile frontend starts up", + "menu_change_to_widget": "Change to widget", + "menu_change_to_frontend_script": "Change to frontend script", + "menu_theme_base": "Theme base" } } diff --git a/apps/client/src/widgets/collections/geomap/index.css b/apps/client/src/widgets/collections/geomap/index.css index 4daaada2cb..a30842c9b6 100644 --- a/apps/client/src/widgets/collections/geomap/index.css +++ b/apps/client/src/widgets/collections/geomap/index.css @@ -11,6 +11,10 @@ } } +body.mobile .geo-view > .collection-properties { + z-index: 2500; +} + .geo-map-container { height: 100%; overflow: hidden; diff --git a/apps/client/src/widgets/layout/ActiveContentBadges.tsx b/apps/client/src/widgets/layout/ActiveContentBadges.tsx new file mode 100644 index 0000000000..00099d0370 --- /dev/null +++ b/apps/client/src/widgets/layout/ActiveContentBadges.tsx @@ -0,0 +1,301 @@ +import { BUILTIN_ATTRIBUTES } from "@triliumnext/commons"; +import clsx from "clsx"; +import { useEffect, useState } from "preact/hooks"; + +import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; +import { t } from "../../services/i18n"; +import { openInAppHelpFromUrl } from "../../services/utils"; +import { BadgeWithDropdown } from "../react/Badge"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import FormToggle from "../react/FormToggle"; +import { useNoteContext, useTriliumEvent } from "../react/hooks"; +import { BookProperty, ViewProperty } from "../react/NotePropertyMenu"; + +const NON_DANGEROUS_ACTIVE_CONTENT = [ "appCss", "appTheme" ]; +const DANGEROUS_ATTRIBUTES = BUILTIN_ATTRIBUTES.filter(a => a.isDangerous || NON_DANGEROUS_ACTIVE_CONTENT.includes(a.name)); +const activeContentLabels = [ "iconPack", "widget", "appCss", "appTheme" ] as const; + +interface ActiveContentInfo { + type: "iconPack" | "backendScript" | "frontendScript" | "widget" | "appCss" | "renderNote" | "webView" | "appTheme"; + isEnabled: boolean; + canToggleEnabled: boolean; +} + +const executeOption: BookProperty = { + type: "button", + icon: "bx bx-play", + label: t("active_content_badges.menu_execute_now"), + onClick: context => context.triggerCommand("runActiveNote") +}; + +const typeMappings: Record = { + iconPack: { + title: t("active_content_badges.type_icon_pack"), + icon: "bx bx-package", + helpPage: "g1mlRoU8CsqC", + }, + backendScript: { + title: t("active_content_badges.type_backend_script"), + icon: "bx bx-server", + helpPage: "SPirpZypehBG", + apiDocsPage: "MEtfsqa5VwNi", + isExecutable: true, + additionalOptions: [ + executeOption, + { + type: "combobox", + bindToLabel: "run", + label: t("active_content_badges.menu_run"), + icon: "bx bx-rss", + dropStart: true, + options: [ + { value: null, label: t("active_content_badges.menu_run_disabled") }, + { value: "backendStartup", label: t("active_content_badges.menu_run_backend_startup") }, + { value: "daily", label: t("active_content_badges.menu_run_daily") }, + { value: "hourly", label: t("active_content_badges.menu_run_hourly") } + ] + } + ] + }, + frontendScript: { + title: t("active_content_badges.type_frontend_script"), + icon: "bx bx-window", + helpPage: "yIhgI5H7A2Sm", + apiDocsPage: "Q2z6av6JZVWm", + isExecutable: true, + additionalOptions: [ + executeOption, + { + type: "combobox", + bindToLabel: "run", + label: t("active_content_badges.menu_run"), + icon: "bx bx-rss", + dropStart: true, + options: [ + { value: null, label: t("active_content_badges.menu_run_disabled") }, + { value: "frontendStartup", label: t("active_content_badges.menu_run_frontend_startup") }, + { value: "mobileStartup", label: t("active_content_badges.menu_run_mobile_startup") }, + ] + }, + { type: "separator" }, + { + type: "button", + label: t("active_content_badges.menu_change_to_widget"), + icon: "bx bxs-widget", + onClick: ({ note }) => attributes.setLabel(note.noteId, "widget") + } + ] + }, + widget: { + title: t("active_content_badges.type_widget"), + icon: "bx bxs-widget", + helpPage: "MgibgPcfeuGz", + additionalOptions: [ + { + type: "button", + label: t("active_content_badges.menu_change_to_frontend_script"), + icon: "bx bx-window", + onClick: ({ note }) => { + attributes.removeOwnedLabelByName(note, "widget"); + attributes.removeOwnedLabelByName(note, "disabled:widget"); + } + } + ] + }, + appCss: { + title: t("active_content_badges.type_app_css"), + icon: "bx bxs-file-css", + helpPage: "AlhDUqhENtH7" + }, + renderNote: { + title: t("active_content_badges.type_render_note"), + icon: "bx bx-extension", + helpPage: "HcABDtFCkbFN" + }, + webView: { + title: t("active_content_badges.type_web_view"), + icon: "bx bx-globe", + helpPage: "1vHRoWCEjj0L" + }, + appTheme: { + title :t("active_content_badges.type_app_theme"), + icon: "bx bx-palette", + helpPage: "7NfNr5pZpVKV", + additionalOptions: [ + { + type: "combobox", + bindToLabel: "appThemeBase", + label: t("active_content_badges.menu_theme_base"), + icon: "bx bx-layer", + dropStart: true, + options: [ + { label: t("theme.auto_theme"), value: null }, + { type: "separator" }, + { label: t("theme.triliumnext"), value: "next" }, + { label: t("theme.triliumnext-light"), value: "next-light" }, + { label: t("theme.triliumnext-dark"), value: "next-dark" } + ] + } + ] + } +}; + +export function ActiveContentBadges() { + const { note } = useNoteContext(); + const info = useActiveContentInfo(note); + + return (note && info && + <> + {info.canToggleEnabled && } + + + ); +} + +function ActiveContentBadge({ info, note }: { note: FNote, info: ActiveContentInfo }) { + const { title, icon, helpPage, apiDocsPage, additionalOptions } = typeMappings[info.type]; + return ( + + {additionalOptions?.length && ( + <> + {additionalOptions?.map((property, i) => ( + + ))} + + + )} + + openInAppHelpFromUrl(helpPage)} + >{t("active_content_badges.menu_docs")} + + {apiDocsPage && openInAppHelpFromUrl(apiDocsPage)} + >{t("code_buttons.trilium_api_docs_button_title")}} + + ); +} + +function ActiveContentToggle({ note, info }: { note: FNote, info: ActiveContentInfo }) { + const { title } = typeMappings[info.type]; + + return info && { + const attrs = note.getOwnedAttributes() + .filter(attr => { + if (attr.isInheritable) return false; + const baseName = getNameWithoutPrefix(attr.name); + return DANGEROUS_ATTRIBUTES.some(item => item.name === baseName && item.type === attr.type); + }); + + for (const attr of attrs) { + const baseName = getNameWithoutPrefix(attr.name); + const newName = willEnable ? baseName : `disabled:${baseName}`; + if (newName === attr.name) continue; + + // We are adding and removing afterwards to avoid a flicker (because for a moment there would be no active content attribute anymore) because the operations are done in sequence and not atomically. + if (attr.type === "label") { + await attributes.setLabel(note.noteId, newName, attr.value); + } else { + await attributes.setRelation(note.noteId, newName, attr.value); + } + await attributes.removeAttributeById(note.noteId, attr.attributeId); + } + }} + />; +} + +function getNameWithoutPrefix(name: string) { + return name.startsWith("disabled:") ? name.substring(9) : name; +} + +function useActiveContentInfo(note: FNote | null | undefined) { + const [ info, setInfo ] = useState(null); + + function refresh() { + let type: ActiveContentInfo["type"] | null = null; + let isEnabled = false; + let canToggleEnabled = false; + + if (!note) { + setInfo(null); + return; + } + + if (note.type === "render") { + type = "renderNote"; + isEnabled = note.hasRelation("renderNote"); + canToggleEnabled = note.hasRelation("renderNote") || note.hasRelation("disabled:renderNote"); + } else if (note.type === "webView") { + type = "webView"; + isEnabled = note.hasLabel("webViewSrc"); + canToggleEnabled = note.hasLabelOrDisabled("webViewSrc"); + } else if (note.type === "code" && note.mime === "application/javascript;env=backend") { + type = "backendScript"; + for (const backendLabel of [ "run", "customRequestHandler", "customResourceProvider" ]) { + isEnabled ||= note.hasLabel(backendLabel); + + if (!canToggleEnabled && note.hasLabelOrDisabled(backendLabel)) { + canToggleEnabled = true; + } + } + } else if (note.type === "code" && note.mime === "application/javascript;env=frontend") { + type = "frontendScript"; + isEnabled = note.hasLabel("widget") || note.hasLabel("run"); + canToggleEnabled = note.hasLabelOrDisabled("widget") || note.hasLabelOrDisabled("run"); + } else if (note.type === "code" && note.hasLabelOrDisabled("appTheme")) { + isEnabled = note.hasLabel("appTheme"); + canToggleEnabled = true; + } + + for (const labelToCheck of activeContentLabels) { + if (note.hasLabel(labelToCheck)) { + type = labelToCheck; + break; + } else if (note.hasLabel(`disabled:${labelToCheck}`)) { + type = labelToCheck; + isEnabled = false; + break; + } + } + + if (type) { + setInfo({ type, isEnabled, canToggleEnabled }); + } else { + setInfo(null); + } + } + + // Refresh on note change. + useEffect(refresh, [ note ]); + + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) { + refresh(); + } + }); + + return info; +} diff --git a/apps/client/src/widgets/layout/NoteBadges.css b/apps/client/src/widgets/layout/NoteBadges.css index ec163da6f2..013963adb4 100644 --- a/apps/client/src/widgets/layout/NoteBadges.css +++ b/apps/client/src/widgets/layout/NoteBadges.css @@ -37,6 +37,10 @@ pointer-events: none; } } + &.active-content-badge { --color: var(--badge-active-content-background-color); } + &.active-content-badge.disabled { + opacity: 0.5; + } min-width: 0; @@ -45,6 +49,11 @@ text-overflow: ellipsis; min-width: 0; } + + .switch-button { + --switch-track-height: 8px; + --switch-track-width: 30px; + } } .dropdown-badge { diff --git a/apps/client/src/widgets/layout/NoteBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx index b4fba9e28a..bf484edf73 100644 --- a/apps/client/src/widgets/layout/NoteBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -10,6 +10,7 @@ import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useGetContextData, useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; import { useShareState } from "../ribbon/BasicPropertiesTab"; import { useShareInfo } from "../shared_info"; +import { ActiveContentBadges } from "./ActiveContentBadges"; export default function NoteBadges() { return ( @@ -19,6 +20,7 @@ export default function NoteBadges() { + ); } diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.tsx b/apps/client/src/widgets/note_bars/CollectionProperties.tsx index 5dba675e6d..66bfb32c45 100644 --- a/apps/client/src/widgets/note_bars/CollectionProperties.tsx +++ b/apps/client/src/widgets/note_bars/CollectionProperties.tsx @@ -2,18 +2,16 @@ import "./CollectionProperties.css"; import { t } from "i18next"; import { ComponentChildren } from "preact"; -import { useContext, useRef } from "preact/hooks"; -import { Fragment } from "preact/jsx-runtime"; +import { useRef } from "preact/hooks"; import FNote from "../../entities/fnote"; import { ViewTypeOptions } from "../collections/interface"; import Dropdown from "../react/Dropdown"; -import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList"; -import FormTextBox from "../react/FormTextBox"; -import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useNoteProperty, useTriliumEvent } from "../react/hooks"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { useNoteProperty, useTriliumEvent } from "../react/hooks"; import Icon from "../react/Icon"; -import { ParentComponent } from "../react/react_utils"; -import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config"; +import { CheckBoxProperty, ViewProperty } from "../react/NotePropertyMenu"; +import { bookPropertiesConfig } from "../ribbon/collection-properties-config"; import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab"; export const ICON_MAPPINGS: Record = { @@ -85,9 +83,11 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption - {properties.map(property => ( - + {properties.map((property, index) => ( + ))} {properties.length > 0 && } @@ -107,127 +107,3 @@ function ViewOptions({ note, viewType }: { note: FNote, viewType: ViewTypeOption ); } - -function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) { - switch (property.type) { - case "button": - return ; - case "split-button": - return ; - case "checkbox": - return ; - case "number": - return ; - case "combobox": - return ; - } -} - -function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) { - const parentComponent = useContext(ParentComponent); - - return ( - { - if (!parentComponent) return; - property.onClick({ - note, - triggerCommand: parentComponent.triggerCommand.bind(parentComponent) - }); - }} - >{property.label} - ); -} - -function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) { - const parentComponent = useContext(ParentComponent); - const ItemsComponent = property.items; - const clickContext = parentComponent && { - note, - triggerCommand: parentComponent.triggerCommand.bind(parentComponent) - }; - - return (parentComponent && - clickContext && property.onClick(clickContext)} - > - - - ); -} - -function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) { - //@ts-expect-error Interop with text box which takes in string values even for numbers. - const [ value, setValue ] = useNoteLabel(note, property.bindToLabel); - const disabled = property.disabled?.(note); - - return ( - e.stopPropagation()} - > - {property.label} - - - ); -} - -function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) { - const [ value, setValue ] = useNoteLabelWithDefault(note, property.bindToLabel, property.defaultValue ?? ""); - - function renderItem(option: ComboBoxItem) { - return ( - setValue(option.value)} - > - {option.label} - - ); - } - - return ( - - {(property.options).map((option, index) => { - if ("items" in option) { - return ( - - {option.title} - {option.items.map(renderItem)} - {index < property.options.length - 1 && } - - ); - } - return renderItem(option); - - })} - - ); -} - -function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) { - const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel); - return ( - - ); -} diff --git a/apps/client/src/widgets/react/NotePropertyMenu.tsx b/apps/client/src/widgets/react/NotePropertyMenu.tsx new file mode 100644 index 0000000000..6d8070741d --- /dev/null +++ b/apps/client/src/widgets/react/NotePropertyMenu.tsx @@ -0,0 +1,210 @@ +import { FilterLabelsByType } from "@triliumnext/commons"; +import { Fragment, VNode } from "preact"; +import { useContext } from "preact/hooks"; + +import Component from "../../components/component"; +import FNote from "../../entities/fnote"; +import NoteContextAwareWidget from "../note_context_aware_widget"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "./FormList"; +import FormTextBox from "./FormTextBox"; +import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault } from "./hooks"; +import { ParentComponent } from "./react_utils"; + +export interface ClickContext { + note: FNote; + triggerCommand: NoteContextAwareWidget["triggerCommand"]; +} + +export interface CheckBoxProperty { + type: "checkbox", + label: string; + bindToLabel: FilterLabelsByType; + icon?: string; +} + +export interface ButtonProperty { + type: "button", + label: string; + title?: string; + icon?: string; + onClick(context: ClickContext): void; +} + +export interface SplitButtonProperty extends Omit { + type: "split-button"; + items({ note, parentComponent }: { note: FNote, parentComponent: Component }): VNode; +} + +export interface NumberProperty { + type: "number", + label: string; + bindToLabel: FilterLabelsByType; + width?: number; + min?: number; + icon?: string; + disabled?: (note: FNote) => boolean; +} + +export interface ComboBoxItem { + /** + * The value to set to the bound label, `null` has a special meaning which removes the label entirely. + */ + value: string | null; + label: string; +} + +export interface ComboBoxGroup { + title: string; + items: ComboBoxItem[]; +} + +interface Separator { + type: "separator" +} + +export interface ComboBoxProperty { + type: "combobox", + label: string; + icon?: string; + bindToLabel: FilterLabelsByType; + /** + * The default value is used when the label is not set. + */ + defaultValue?: string; + options: (ComboBoxItem | Separator | ComboBoxGroup)[]; + dropStart?: boolean; +} + +export type BookProperty = CheckBoxProperty | ButtonProperty | NumberProperty | ComboBoxProperty | SplitButtonProperty | Separator; + +export function ViewProperty({ note, property }: { note: FNote, property: BookProperty }) { + switch (property.type) { + case "button": + return ; + case "split-button": + return ; + case "checkbox": + return ; + case "number": + return ; + case "combobox": + return ; + case "separator": + return ; + } +} + +function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) { + const parentComponent = useContext(ParentComponent); + + return ( + { + if (!parentComponent) return; + property.onClick({ + note, + triggerCommand: parentComponent.triggerCommand.bind(parentComponent) + }); + }} + >{property.label} + ); +} + +function SplitButtonPropertyView({ note, property }: { note: FNote, property: SplitButtonProperty }) { + const parentComponent = useContext(ParentComponent); + const ItemsComponent = property.items; + const clickContext = parentComponent && { + note, + triggerCommand: parentComponent.triggerCommand.bind(parentComponent) + }; + + return (parentComponent && + clickContext && property.onClick(clickContext)} + > + + + ); +} + +function NumberPropertyView({ note, property }: { note: FNote, property: NumberProperty }) { + //@ts-expect-error Interop with text box which takes in string values even for numbers. + const [ value, setValue ] = useNoteLabel(note, property.bindToLabel); + const disabled = property.disabled?.(note); + + return ( + e.stopPropagation()} + > + {property.label} + + + ); +} + +function ComboBoxPropertyView({ note, property }: { note: FNote, property: ComboBoxProperty }) { + const [ value, setValue ] = useNoteLabel(note, property.bindToLabel); + const valueWithDefault = value ?? property.defaultValue ?? null; + + function renderItem(option: ComboBoxItem) { + return ( + setValue(option.value)} + > + {option.label} + + ); + } + + return ( + + {(property.options).map((option, index) => { + if ("items" in option) { + return ( + + {option.title} + {option.items.map(renderItem)} + {index < property.options.length - 1 && } + + ); + } + if ("type" in option) { + return ; + } + + return renderItem(option); + + })} + + ); +} + +function CheckBoxPropertyView({ note, property }: { note: FNote, property: CheckBoxProperty }) { + const [ value, setValue ] = useNoteLabelBoolean(note, property.bindToLabel); + return ( + + ); +} diff --git a/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx b/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx index 4af8247a3f..e25f2650c9 100644 --- a/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/CollectionPropertiesTab.tsx @@ -1,18 +1,20 @@ -import { useContext, useMemo } from "preact/hooks"; -import { t } from "../../services/i18n"; -import FormSelect, { FormSelectWithGroups } from "../react/FormSelect"; -import { TabContext } from "./ribbon-interface"; -import { mapToKeyValueArray } from "../../services/utils"; -import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; -import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "./collection-properties-config"; -import Button, { SplitButton } from "../react/Button"; -import { ParentComponent } from "../react/react_utils"; -import FNote from "../../entities/fnote"; -import FormCheckbox from "../react/FormCheckbox"; -import FormTextBox from "../react/FormTextBox"; import { ComponentChildren } from "preact"; -import { ViewTypeOptions } from "../collections/interface"; +import { useContext, useMemo } from "preact/hooks"; + +import FNote from "../../entities/fnote"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; +import { t } from "../../services/i18n"; +import { mapToKeyValueArray } from "../../services/utils"; +import { ViewTypeOptions } from "../collections/interface"; +import Button, { SplitButton } from "../react/Button"; +import FormCheckbox from "../react/FormCheckbox"; +import FormSelect, { FormSelectWithGroups } from "../react/FormSelect"; +import FormTextBox from "../react/FormTextBox"; +import { useNoteLabel, useNoteLabelBoolean } from "../react/hooks"; +import { BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxGroup, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../react/NotePropertyMenu"; +import { ParentComponent } from "../react/react_utils"; +import { bookPropertiesConfig } from "./collection-properties-config"; +import { TabContext } from "./ribbon-interface"; export const VIEW_TYPE_MAPPINGS: Record = { grid: t("book_properties.grid"), @@ -50,70 +52,70 @@ export function useViewType(note: FNote | null | undefined) { } function CollectionTypeSwitcher({ viewType, setViewType }: { viewType: string, setViewType: (newValue: string) => void }) { - const collectionTypes = useMemo(() => mapToKeyValueArray(VIEW_TYPE_MAPPINGS), []); + const collectionTypes = useMemo(() => mapToKeyValueArray(VIEW_TYPE_MAPPINGS), []); - return ( -
- {t("book_properties.view_type")}:    - -
- ) + return ( +
+ {t("book_properties.view_type")}:    + +
+ ); } -function BookProperties({ viewType, note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) { - return ( - <> - {properties.map(property => ( -
- {mapPropertyView({ note, property })} -
- ))} +function BookProperties({ note, properties }: { viewType: ViewTypeOptions, note: FNote, properties: BookProperty[] }) { + return ( + <> + {properties.map((property, index) => ( +
+ {mapPropertyView({ note, property })} +
+ ))} - - - ) + + + ); } function mapPropertyView({ note, property }: { note: FNote, property: BookProperty }) { - switch (property.type) { - case "button": - return - case "split-button": - return - case "checkbox": - return - case "number": - return - case "combobox": - return - } + switch (property.type) { + case "button": + return ; + case "split-button": + return ; + case "checkbox": + return ; + case "number": + return ; + case "combobox": + return ; + } } function ButtonPropertyView({ note, property }: { note: FNote, property: ButtonProperty }) { - const parentComponent = useContext(ParentComponent); + const parentComponent = useContext(ParentComponent); - return