implement auto reconnect
This commit is contained in:
@@ -4,29 +4,32 @@ import BoardViewModel from "../../ViewModel/Pages/boardViewModel";
|
||||
import { allowDrop } from "../utility";
|
||||
|
||||
export function BoardEntry(boardViewModel: BoardViewModel) {
|
||||
const view = (
|
||||
<button
|
||||
set:color={boardViewModel.color}
|
||||
class="tile colored-tile"
|
||||
toggle:selected={boardViewModel.isSelected}
|
||||
on:click={boardViewModel.select}
|
||||
on:dragover={allowDrop}
|
||||
on:drop={boardViewModel.handleDropBetweenBoards}
|
||||
>
|
||||
<span class="shadow" subscribe:innerText={boardViewModel.name}></span>
|
||||
<b subscribe:innerText={boardViewModel.name}></b>
|
||||
</button>
|
||||
);
|
||||
const view = (
|
||||
<button
|
||||
set:color={boardViewModel.color}
|
||||
class="tile colored-tile"
|
||||
toggle:selected={boardViewModel.isSelected}
|
||||
on:click={boardViewModel.select}
|
||||
on:dragover={allowDrop}
|
||||
on:drop={boardViewModel.handleDropBetweenBoards}
|
||||
>
|
||||
<span
|
||||
class="shadow"
|
||||
subscribe:innerText={boardViewModel.name}
|
||||
></span>
|
||||
<b subscribe:innerText={boardViewModel.name}></b>
|
||||
</button>
|
||||
);
|
||||
|
||||
boardViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
boardViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
|
||||
return view;
|
||||
return view;
|
||||
}
|
||||
|
||||
export const BoardViewModelToEntry: React.StateItemConverter<BoardViewModel> = (
|
||||
boardViewModel: BoardViewModel
|
||||
boardViewModel: BoardViewModel,
|
||||
) => {
|
||||
return BoardEntry(boardViewModel);
|
||||
return BoardEntry(boardViewModel);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import BoardViewModel, {
|
||||
BoardPageType,
|
||||
BoardPageType,
|
||||
} from "../../ViewModel/Pages/boardViewModel";
|
||||
|
||||
import { RibbonButton } from "./ribbonButton";
|
||||
|
||||
export function BoardViewToggleButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
page: BoardPageType,
|
||||
boardViewModel: BoardViewModel
|
||||
label: string,
|
||||
icon: string,
|
||||
page: BoardPageType,
|
||||
boardViewModel: BoardViewModel,
|
||||
) {
|
||||
function select() {
|
||||
boardViewModel.selectedPage.value = page;
|
||||
}
|
||||
function select() {
|
||||
boardViewModel.selectedPage.value = page;
|
||||
}
|
||||
|
||||
const isSelected = React.createProxyState(
|
||||
[boardViewModel.selectedPage],
|
||||
() => boardViewModel.selectedPage.value == page
|
||||
);
|
||||
const isSelected = React.createProxyState(
|
||||
[boardViewModel.selectedPage],
|
||||
() => boardViewModel.selectedPage.value == page,
|
||||
);
|
||||
|
||||
return RibbonButton(label, icon, isSelected, select);
|
||||
return RibbonButton(label, icon, isSelected, select);
|
||||
}
|
||||
|
||||
@@ -3,33 +3,37 @@ import * as React from "bloatless-react";
|
||||
import ChatViewModel from "../../ViewModel/Chat/chatViewModel";
|
||||
|
||||
export function ChatEntry(chatViewModel: ChatViewModel) {
|
||||
const view = (
|
||||
<button
|
||||
class="tile colored-tile chat-entry"
|
||||
set:color={chatViewModel.settingsPageViewModel.color}
|
||||
style="height: 8rem"
|
||||
on:click={chatViewModel.open}
|
||||
toggle:unread={chatViewModel.hasUnreadMessages}
|
||||
>
|
||||
<span
|
||||
class="shadow"
|
||||
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
|
||||
></span>
|
||||
<h2
|
||||
subscribe:innerText={chatViewModel.settingsPageViewModel.primaryChannel}
|
||||
></h2>
|
||||
</button>
|
||||
);
|
||||
const view = (
|
||||
<button
|
||||
class="tile colored-tile chat-entry"
|
||||
set:color={chatViewModel.settingsPageViewModel.color}
|
||||
style="height: 8rem"
|
||||
on:click={chatViewModel.open}
|
||||
toggle:unread={chatViewModel.hasUnreadMessages}
|
||||
>
|
||||
<span
|
||||
class="shadow"
|
||||
subscribe:innerText={
|
||||
chatViewModel.settingsPageViewModel.primaryChannel
|
||||
}
|
||||
></span>
|
||||
<h2
|
||||
subscribe:innerText={
|
||||
chatViewModel.settingsPageViewModel.primaryChannel
|
||||
}
|
||||
></h2>
|
||||
</button>
|
||||
);
|
||||
|
||||
chatViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
chatViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
|
||||
return view;
|
||||
return view;
|
||||
}
|
||||
|
||||
export const ChatViewModelToChatEntry: React.StateItemConverter<
|
||||
ChatViewModel
|
||||
ChatViewModel
|
||||
> = (chatViewModel: ChatViewModel) => {
|
||||
return ChatEntry(chatViewModel);
|
||||
return ChatEntry(chatViewModel);
|
||||
};
|
||||
|
||||
@@ -6,61 +6,65 @@ import ChatMessageViewModel from "../../ViewModel/Chat/chatMessageViewModel";
|
||||
import { translations } from "../translations";
|
||||
|
||||
export function ChatMessage(chatMessageViewModel: ChatMessageViewModel) {
|
||||
const statusIcon = React.createProxyState(
|
||||
[chatMessageViewModel.status],
|
||||
() => {
|
||||
switch (chatMessageViewModel.status.value) {
|
||||
case ChatMessageStatus.Outbox:
|
||||
return "hourglass_top";
|
||||
case ChatMessageStatus.Sent:
|
||||
return "check";
|
||||
case ChatMessageStatus.Received:
|
||||
return "done_all";
|
||||
default:
|
||||
return "warning";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
class="message-bubble"
|
||||
toggle:sentbyuser={chatMessageViewModel.sentByUser}
|
||||
>
|
||||
<div class="main tile">
|
||||
<div class="text-container">
|
||||
<span class="sender-name ellipsis">
|
||||
{chatMessageViewModel.sender}
|
||||
</span>
|
||||
<span
|
||||
class="body"
|
||||
subscribe:innerText={chatMessageViewModel.body}
|
||||
></span>
|
||||
<span class="timestamp ellipsis">
|
||||
<span class="icon" subscribe:innerText={statusIcon}></span>
|
||||
{chatMessageViewModel.dateSent}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button
|
||||
on:click={chatMessageViewModel.showInfoModal}
|
||||
aria-label={
|
||||
translations.chatPage.message.showMessageInfoButtonAudioLabel
|
||||
const statusIcon = React.createProxyState(
|
||||
[chatMessageViewModel.status],
|
||||
() => {
|
||||
switch (chatMessageViewModel.status.value) {
|
||||
case ChatMessageStatus.Outbox:
|
||||
return "hourglass_top";
|
||||
case ChatMessageStatus.Sent:
|
||||
return "check";
|
||||
case ChatMessageStatus.Received:
|
||||
return "done_all";
|
||||
default:
|
||||
return "warning";
|
||||
}
|
||||
>
|
||||
<span class="icon">info</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
);
|
||||
|
||||
{ChatMessageInfoModal(chatMessageViewModel)}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div
|
||||
class="message-bubble"
|
||||
toggle:sentbyuser={chatMessageViewModel.sentByUser}
|
||||
>
|
||||
<div class="main tile">
|
||||
<div class="text-container">
|
||||
<span class="sender-name ellipsis">
|
||||
{chatMessageViewModel.sender}
|
||||
</span>
|
||||
<span
|
||||
class="body"
|
||||
subscribe:innerText={chatMessageViewModel.body}
|
||||
></span>
|
||||
<span class="timestamp ellipsis">
|
||||
<span
|
||||
class="icon"
|
||||
subscribe:innerText={statusIcon}
|
||||
></span>
|
||||
{chatMessageViewModel.dateSent}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="button-container">
|
||||
<button
|
||||
on:click={chatMessageViewModel.showInfoModal}
|
||||
aria-label={
|
||||
translations.chatPage.message
|
||||
.showMessageInfoButtonAudioLabel
|
||||
}
|
||||
>
|
||||
<span class="icon">info</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ChatMessageInfoModal(chatMessageViewModel)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ChatMessageViewModelToView: React.StateItemConverter<
|
||||
ChatMessageViewModel
|
||||
ChatMessageViewModel
|
||||
> = (chatMessageViewModel: ChatMessageViewModel) => {
|
||||
return ChatMessage(chatMessageViewModel);
|
||||
return ChatMessage(chatMessageViewModel);
|
||||
};
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import ChatViewModel, {
|
||||
ChatPageType,
|
||||
ChatPageType,
|
||||
} from "../../ViewModel/Chat/chatViewModel";
|
||||
|
||||
import { RibbonButton } from "./ribbonButton";
|
||||
|
||||
export function ChatViewToggleButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
page: ChatPageType,
|
||||
chatViewModel: ChatViewModel
|
||||
label: string,
|
||||
icon: string,
|
||||
page: ChatPageType,
|
||||
chatViewModel: ChatViewModel,
|
||||
) {
|
||||
function select() {
|
||||
chatViewModel.selectedPage.value = page;
|
||||
}
|
||||
function select() {
|
||||
chatViewModel.selectedPage.value = page;
|
||||
}
|
||||
|
||||
const isSelected = React.createProxyState(
|
||||
[chatViewModel.selectedPage],
|
||||
() => chatViewModel.selectedPage.value == page
|
||||
);
|
||||
const isSelected = React.createProxyState(
|
||||
[chatViewModel.selectedPage],
|
||||
() => chatViewModel.selectedPage.value == page,
|
||||
);
|
||||
|
||||
return RibbonButton(label, icon, isSelected, select);
|
||||
return RibbonButton(label, icon, isSelected, select);
|
||||
}
|
||||
|
||||
@@ -3,28 +3,28 @@ import * as React from "bloatless-react";
|
||||
import { Color } from "../../colors";
|
||||
|
||||
export function ColorPicker(selectedColor: React.State<Color>) {
|
||||
return (
|
||||
<div class="flex-row gap width-input">
|
||||
{...Object.values(Color).map((color) => {
|
||||
const isSelected = React.createProxyState(
|
||||
[selectedColor],
|
||||
() => selectedColor.value == color
|
||||
);
|
||||
return (
|
||||
<div class="flex-row gap width-input">
|
||||
{...Object.values(Color).map((color) => {
|
||||
const isSelected = React.createProxyState(
|
||||
[selectedColor],
|
||||
() => selectedColor.value == color,
|
||||
);
|
||||
|
||||
function setColor() {
|
||||
selectedColor.value = color;
|
||||
}
|
||||
function setColor() {
|
||||
selectedColor.value = color;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
color={color}
|
||||
class="fill-color width-100 flex"
|
||||
style="height: 2rem"
|
||||
toggle:selected={isSelected}
|
||||
on:click={setColor}
|
||||
></button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
color={color}
|
||||
class="fill-color width-100 flex"
|
||||
style="height: 2rem"
|
||||
toggle:selected={isSelected}
|
||||
on:click={setColor}
|
||||
></button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,45 +3,45 @@ import * as React from "bloatless-react";
|
||||
import { translations } from "../translations";
|
||||
|
||||
export function DangerousActionButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
action: () => void
|
||||
label: string,
|
||||
icon: string,
|
||||
action: () => void,
|
||||
) {
|
||||
const isActionRequested = new React.State(false);
|
||||
const cannotConfirm = React.createProxyState(
|
||||
[isActionRequested],
|
||||
() => isActionRequested.value == false
|
||||
);
|
||||
const isActionRequested = new React.State(false);
|
||||
const cannotConfirm = React.createProxyState(
|
||||
[isActionRequested],
|
||||
() => isActionRequested.value == false,
|
||||
);
|
||||
|
||||
function requestAction() {
|
||||
isActionRequested.value = true;
|
||||
}
|
||||
function abort() {
|
||||
isActionRequested.value = false;
|
||||
}
|
||||
function requestAction() {
|
||||
isActionRequested.value = true;
|
||||
}
|
||||
function abort() {
|
||||
isActionRequested.value = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex-row">
|
||||
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
|
||||
{translations.general.abortButton}
|
||||
<span class="icon">undo</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger flex"
|
||||
on:click={requestAction}
|
||||
toggle:hidden={isActionRequested}
|
||||
>
|
||||
{label}
|
||||
<span class="icon">{icon}</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger flex"
|
||||
on:click={action}
|
||||
toggle:hidden={cannotConfirm}
|
||||
>
|
||||
{translations.general.confirmButton}
|
||||
<span class="icon">warning</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div class="flex-row">
|
||||
<button class="flex" on:click={abort} toggle:hidden={cannotConfirm}>
|
||||
{translations.general.abortButton}
|
||||
<span class="icon">undo</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger flex"
|
||||
on:click={requestAction}
|
||||
toggle:hidden={isActionRequested}
|
||||
>
|
||||
{label}
|
||||
<span class="icon">{icon}</span>
|
||||
</button>
|
||||
<button
|
||||
class="danger flex"
|
||||
on:click={action}
|
||||
toggle:hidden={cannotConfirm}
|
||||
>
|
||||
{translations.general.confirmButton}
|
||||
<span class="icon">warning</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,25 +3,25 @@ import * as React from "bloatless-react";
|
||||
import { translations } from "../translations";
|
||||
|
||||
export function DeletableListItem(
|
||||
text: string,
|
||||
primaryButton: HTMLElement,
|
||||
ondelete: () => void
|
||||
text: string,
|
||||
primaryButton: HTMLElement,
|
||||
ondelete: () => void,
|
||||
) {
|
||||
return (
|
||||
<div class="tile flex-row justify-apart align-center padding-0">
|
||||
<span class="padding-h ellipsis">{text}</span>
|
||||
return (
|
||||
<div class="tile flex-row justify-apart align-center padding-0">
|
||||
<span class="padding-h ellipsis">{text}</span>
|
||||
|
||||
<div class="flex-row justify-end">
|
||||
{primaryButton}
|
||||
<div class="flex-row justify-end">
|
||||
{primaryButton}
|
||||
|
||||
<button
|
||||
class="danger"
|
||||
aria-label={translations.general.deleteItemButtonAudioLabel}
|
||||
on:click={ondelete}
|
||||
>
|
||||
<span class="icon">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
<button
|
||||
class="danger"
|
||||
aria-label={translations.general.deleteItemButtonAudioLabel}
|
||||
on:click={ondelete}
|
||||
>
|
||||
<span class="icon">delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import StorageModel, {
|
||||
PATH_COMPONENT_SEPARATOR,
|
||||
PATH_COMPONENT_SEPARATOR,
|
||||
} from "../../Model/Global/storageModel";
|
||||
|
||||
import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
||||
|
||||
export function DirectoryItemList(
|
||||
storageViewModel: StorageViewModel,
|
||||
pathString: string = PATH_COMPONENT_SEPARATOR
|
||||
storageViewModel: StorageViewModel,
|
||||
pathString: string = PATH_COMPONENT_SEPARATOR,
|
||||
) {
|
||||
// converter
|
||||
const StringToDirectoryItemList: React.StateItemConverter<string> = (
|
||||
pathString: string
|
||||
) => DirectoryItemList(storageViewModel, pathString);
|
||||
// converter
|
||||
const StringToDirectoryItemList: React.StateItemConverter<string> = (
|
||||
pathString: string,
|
||||
) => DirectoryItemList(storageViewModel, pathString);
|
||||
|
||||
// data
|
||||
const path = StorageModel.stringToPathComponents(pathString);
|
||||
const fileName = StorageModel.getFileName(path);
|
||||
const items = new React.ListState<string>();
|
||||
// data
|
||||
const path = StorageModel.stringToPathComponents(pathString);
|
||||
const fileName = StorageModel.getFileName(path);
|
||||
const items = new React.ListState<string>();
|
||||
|
||||
const style = `text-indent: ${path.length}rem`;
|
||||
const style = `text-indent: ${path.length}rem`;
|
||||
|
||||
// methods
|
||||
function loadItems() {
|
||||
items.clear();
|
||||
// methods
|
||||
function loadItems() {
|
||||
items.clear();
|
||||
|
||||
const directoryItems = storageViewModel.storageModel.list(path);
|
||||
const directoryItems = storageViewModel.storageModel.list(path);
|
||||
|
||||
for (const directoryItem of directoryItems) {
|
||||
const itemPath = [...path, directoryItem];
|
||||
const pathString = StorageModel.pathComponentsToString(...itemPath);
|
||||
items.add(pathString);
|
||||
for (const directoryItem of directoryItems) {
|
||||
const itemPath = [...path, directoryItem];
|
||||
const pathString = StorageModel.pathComponentsToString(...itemPath);
|
||||
items.add(pathString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function select() {
|
||||
storageViewModel.selectedPath.value = pathString;
|
||||
}
|
||||
function select() {
|
||||
storageViewModel.selectedPath.value = pathString;
|
||||
}
|
||||
|
||||
// handlers
|
||||
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
|
||||
if (!items.value.has(lastDeletedItemPath)) return;
|
||||
// handlers
|
||||
storageViewModel.lastDeletedItemPath.subscribe((lastDeletedItemPath) => {
|
||||
if (!items.value.has(lastDeletedItemPath)) return;
|
||||
|
||||
select();
|
||||
setTimeout(() => loadItems(), 50);
|
||||
});
|
||||
select();
|
||||
setTimeout(() => loadItems(), 50);
|
||||
});
|
||||
|
||||
// state
|
||||
const isSelected = React.createProxyState(
|
||||
[storageViewModel.selectedPath],
|
||||
() => storageViewModel.selectedPath.value == pathString
|
||||
);
|
||||
// state
|
||||
const isSelected = React.createProxyState(
|
||||
[storageViewModel.selectedPath],
|
||||
() => storageViewModel.selectedPath.value == pathString,
|
||||
);
|
||||
|
||||
isSelected.subscribe(() => {
|
||||
if (isSelected.value == false) return;
|
||||
loadItems();
|
||||
});
|
||||
isSelected.subscribe(() => {
|
||||
if (isSelected.value == false) return;
|
||||
loadItems();
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="flex-column">
|
||||
<button
|
||||
class="width-100 flex-1 clip"
|
||||
toggle:selected={isSelected}
|
||||
on:click={select}
|
||||
>
|
||||
<span class="ellipsis width-100 flex-1" style={style}>
|
||||
{fileName}
|
||||
</span>
|
||||
</button>
|
||||
return (
|
||||
<div class="flex-column">
|
||||
<button
|
||||
class="width-100 flex-1 clip"
|
||||
toggle:selected={isSelected}
|
||||
on:click={select}
|
||||
>
|
||||
<span class="ellipsis width-100 flex-1" style={style}>
|
||||
{fileName}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="flex-column"
|
||||
children:append={[items, StringToDirectoryItemList]}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
<div
|
||||
class="flex-column"
|
||||
children:append={[items, StringToDirectoryItemList]}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
.file-browser {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
display: flex;
|
||||
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: x mandatory;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.file-browser > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
flex: 0;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
flex: 0;
|
||||
|
||||
scroll-snap-align: center;
|
||||
scroll-snap-align: center;
|
||||
}
|
||||
|
||||
.file-browser .scroll-area {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
padding: var(--content-padding);
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
.file-browser .detail-button-wrapper {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
|
||||
border-top: 1px solid var(--border);
|
||||
padding: var(--content-padding);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: var(--content-padding);
|
||||
}
|
||||
|
||||
.file-browser .detail-button-wrapper button {
|
||||
max-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.file-browser code {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-size: .9rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.file-browser > div {
|
||||
width: 50%;
|
||||
min-width: 50%;
|
||||
.file-browser > div {
|
||||
width: 50%;
|
||||
min-width: 50%;
|
||||
|
||||
flex: 1;
|
||||
}
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-browser .detail-button-wrapper {
|
||||
display: none;
|
||||
}
|
||||
.file-browser .detail-button-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,75 +9,85 @@ import StorageViewModel from "../../ViewModel/Global/storageViewModel";
|
||||
import { translations } from "../translations";
|
||||
|
||||
export function FileBrowser(storageViewModel: StorageViewModel) {
|
||||
const detailView = React.createProxyState(
|
||||
[storageViewModel.selectedPath],
|
||||
() => {
|
||||
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
|
||||
return (
|
||||
<div>
|
||||
<span class="secondary">{translations.storage.noItemSelected}</span>
|
||||
const detailView = React.createProxyState(
|
||||
[storageViewModel.selectedPath],
|
||||
() => {
|
||||
if (storageViewModel.selectedPath.value == PATH_COMPONENT_SEPARATOR)
|
||||
return (
|
||||
<div>
|
||||
<span class="secondary">
|
||||
{translations.storage.noItemSelected}
|
||||
</span>
|
||||
|
||||
<hr></hr>
|
||||
<hr></hr>
|
||||
|
||||
{DangerousActionButton(
|
||||
translations.storage.removeJunkButton,
|
||||
"delete_forever",
|
||||
storageViewModel.removeJunk
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
{DangerousActionButton(
|
||||
translations.storage.removeJunkButton,
|
||||
"delete_forever",
|
||||
storageViewModel.removeJunk,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div class="flex-column gap">
|
||||
<div class="tile flex-no">
|
||||
return (
|
||||
<div class="flex-column gap">
|
||||
<div class="tile flex-no">
|
||||
<div>
|
||||
<b>{translations.storage.path}</b>
|
||||
<span
|
||||
class="break-all"
|
||||
subscribe:innerText={
|
||||
storageViewModel.selectedPath
|
||||
}
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile flex-no">
|
||||
<div>
|
||||
<b>{translations.storage.content}</b>
|
||||
<code
|
||||
subscribe:innerText={
|
||||
storageViewModel.selectedFileContent
|
||||
}
|
||||
></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{DangerousActionButton(
|
||||
translations.storage.deleteItem,
|
||||
"delete_forever",
|
||||
storageViewModel.deleteSelectedItem,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const view = (
|
||||
<div class="file-browser">
|
||||
<div>
|
||||
<b>{translations.storage.path}</b>
|
||||
<span
|
||||
class="break-all"
|
||||
subscribe:innerText={storageViewModel.selectedPath}
|
||||
></span>
|
||||
<div class="scroll-area">
|
||||
{DirectoryItemList(storageViewModel)}
|
||||
</div>
|
||||
<div class="detail-button-wrapper">
|
||||
<button class="ghost" on:click={scrollToDetails}>
|
||||
<span
|
||||
class="ellipsis"
|
||||
subscribe:innerText={
|
||||
storageViewModel.selectedFileName
|
||||
}
|
||||
></span>
|
||||
<span class="icon">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile flex-no">
|
||||
<div>
|
||||
<b>{translations.storage.content}</b>
|
||||
<code
|
||||
subscribe:innerText={storageViewModel.selectedFileContent}
|
||||
></code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{DangerousActionButton(
|
||||
translations.storage.deleteItem,
|
||||
"delete_forever",
|
||||
storageViewModel.deleteSelectedItem
|
||||
)}
|
||||
<div class="scroll-area" children:set={detailView}></div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
|
||||
function scrollToDetails() {
|
||||
view.scrollLeft = view.scrollWidth;
|
||||
}
|
||||
);
|
||||
|
||||
const view = (
|
||||
<div class="file-browser">
|
||||
<div>
|
||||
<div class="scroll-area">{DirectoryItemList(storageViewModel)}</div>
|
||||
<div class="detail-button-wrapper">
|
||||
<button class="ghost" on:click={scrollToDetails}>
|
||||
<span
|
||||
class="ellipsis"
|
||||
subscribe:innerText={storageViewModel.selectedFileName}
|
||||
></span>
|
||||
<span class="icon">arrow_forward</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll-area" children:set={detailView}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function scrollToDetails() {
|
||||
view.scrollLeft = view.scrollWidth;
|
||||
}
|
||||
|
||||
return view;
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import {
|
||||
StringEntryObject,
|
||||
checkDoesObjectMatchReference,
|
||||
StringEntryObject,
|
||||
checkDoesObjectMatchReference,
|
||||
} from "../../Model/Utility/utility";
|
||||
|
||||
export function FilteredList<T>(
|
||||
reference: StringEntryObject,
|
||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||
objects: React.ListState<T> | React.MapState<T>,
|
||||
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement
|
||||
reference: StringEntryObject,
|
||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||
objects: React.ListState<T> | React.MapState<T>,
|
||||
viewBuilder: (matchingObjects: React.ListState<T>) => HTMLElement,
|
||||
) {
|
||||
const matchingObjects: React.ListState<T> = new React.ListState();
|
||||
const matchingObjects: React.ListState<T> = new React.ListState();
|
||||
|
||||
objects.handleAddition((newObject) => {
|
||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||
reference,
|
||||
stringEntryObjectConverter(newObject)
|
||||
);
|
||||
if (doesMatch == false) return;
|
||||
objects.handleAddition((newObject) => {
|
||||
const doesMatch: boolean = checkDoesObjectMatchReference(
|
||||
reference,
|
||||
stringEntryObjectConverter(newObject),
|
||||
);
|
||||
if (doesMatch == false) return;
|
||||
|
||||
matchingObjects.add(newObject);
|
||||
objects.handleRemoval(newObject, () => {
|
||||
matchingObjects.remove(newObject);
|
||||
matchingObjects.add(newObject);
|
||||
objects.handleRemoval(newObject, () => {
|
||||
matchingObjects.remove(newObject);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return viewBuilder(matchingObjects);
|
||||
return viewBuilder(matchingObjects);
|
||||
}
|
||||
|
||||
@@ -1,106 +1,106 @@
|
||||
.month-grid-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: var(--gap-between-tiles);
|
||||
gap: var(--gap-between-tiles);
|
||||
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.month-grid-wrapper .day-labels {
|
||||
flex: 0;
|
||||
flex: 0;
|
||||
|
||||
padding: var(--content-padding);
|
||||
padding-bottom: 0px;
|
||||
padding: var(--content-padding);
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.month-grid-wrapper > div {
|
||||
display: grid;
|
||||
gap: var(--gap-between-tiles);
|
||||
grid-template-columns: repeat(7, minmax(0px, 1fr));
|
||||
display: grid;
|
||||
gap: var(--gap-between-tiles);
|
||||
grid-template-columns: repeat(7, minmax(0px, 1fr));
|
||||
|
||||
animation: slide-up var(--animation-duration);
|
||||
animation: slide-up var(--animation-duration);
|
||||
}
|
||||
|
||||
.month-grid {
|
||||
overflow: visible;
|
||||
overflow: visible;
|
||||
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0px;
|
||||
padding: var(--content-padding);
|
||||
padding-top: 0px;
|
||||
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
|
||||
grid-auto-rows: 1fr;
|
||||
grid-auto-rows: 1fr;
|
||||
}
|
||||
|
||||
.month-grid .tile {
|
||||
animation: slide-up var(--animation-duration);
|
||||
overflow: hidden;
|
||||
animation: slide-up var(--animation-duration);
|
||||
overflow: hidden;
|
||||
|
||||
align-items: start;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.month-grid .tile[today] b {
|
||||
color: var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.month-grid .tile > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.month-grid .tile .event-count {
|
||||
display: none;
|
||||
display: none;
|
||||
|
||||
background-color: var(--text-normal);
|
||||
color: var(--background);
|
||||
background-color: var(--text-normal);
|
||||
color: var(--background);
|
||||
|
||||
opacity: 0.6;
|
||||
border-radius: 100%;
|
||||
opacity: 0.6;
|
||||
border-radius: 100%;
|
||||
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 0.6rem;
|
||||
font-weight: bold;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
font-size: 0.6rem;
|
||||
font-weight: bold;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.month-grid .tile .event-count[has-events] {
|
||||
background-color: var(--primary);
|
||||
opacity: 1;
|
||||
background-color: var(--primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.month-grid .tile .event-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
gap: var(--gap-between-tiles);
|
||||
gap: var(--gap-between-tiles);
|
||||
}
|
||||
.month-grid .tile .event-list span {
|
||||
animation: slide-up var(--animation-duration);
|
||||
animation: slide-up var(--animation-duration);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) and (max-height: 900px) {
|
||||
.month-grid .tile {
|
||||
padding: 0px;
|
||||
}
|
||||
.month-grid .tile {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.month-grid .tile > div {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.month-grid .tile > div {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.month-grid .tile .event-count {
|
||||
display: flex;
|
||||
}
|
||||
.month-grid .tile .event-count {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.month-grid .tile .event-list {
|
||||
display: none;
|
||||
}
|
||||
.month-grid .tile .event-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,103 +8,105 @@ import { allowDrop } from "../utility";
|
||||
import { localeCompare } from "../../Model/Utility/utility";
|
||||
|
||||
export function MonthGrid<T>(
|
||||
monthGrid: MonthGrid<React.MapState<T>>,
|
||||
selectedDate: React.State<number>,
|
||||
handleDrop: (date: string) => void
|
||||
monthGrid: MonthGrid<React.MapState<T>>,
|
||||
selectedDate: React.State<number>,
|
||||
handleDrop: (date: string) => void,
|
||||
) {
|
||||
const dayLabels: HTMLElement[] = [];
|
||||
let currentWeekday = monthGrid.firstDayOfWeek;
|
||||
while (dayLabels.length < 7) {
|
||||
dayLabels.push(
|
||||
<span class="secondary">
|
||||
{translations.regional.weekdays.abbreviated[currentWeekday]}
|
||||
</span>
|
||||
const dayLabels: HTMLElement[] = [];
|
||||
let currentWeekday = monthGrid.firstDayOfWeek;
|
||||
while (dayLabels.length < 7) {
|
||||
dayLabels.push(
|
||||
<span class="secondary">
|
||||
{translations.regional.weekdays.abbreviated[currentWeekday]}
|
||||
</span>,
|
||||
);
|
||||
currentWeekday++;
|
||||
if (currentWeekday == 7) currentWeekday = 0;
|
||||
}
|
||||
|
||||
const offsetElements: HTMLElement[] = [];
|
||||
for (let i = 0; i < monthGrid.offset; i++) {
|
||||
offsetElements.push(<div></div>);
|
||||
}
|
||||
|
||||
const converter: React.StateItemConverter<TaskViewModel> = (
|
||||
taskViewModel,
|
||||
) => {
|
||||
const view = (
|
||||
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
|
||||
);
|
||||
|
||||
taskViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="month-grid-wrapper">
|
||||
<div class="day-labels">{...dayLabels}</div>
|
||||
<div class="month-grid">
|
||||
{...offsetElements}
|
||||
{...Object.entries(monthGrid.days)
|
||||
.sort((a, b) => localeCompare(a[0], b[0]))
|
||||
.map((entry) => {
|
||||
const [date, mapState] = entry;
|
||||
|
||||
const isSelected = React.createProxyState(
|
||||
[selectedDate],
|
||||
() => selectedDate.value == parseInt(date),
|
||||
);
|
||||
|
||||
const isToday: boolean =
|
||||
monthGrid.isCurrentMonth == true &&
|
||||
parseInt(date) == new Date().getDate();
|
||||
|
||||
const eventCount: React.State<number> =
|
||||
React.createProxyState(
|
||||
[mapState],
|
||||
() => mapState.value.size,
|
||||
);
|
||||
|
||||
const hasEvents: React.State<boolean> =
|
||||
React.createProxyState(
|
||||
[eventCount],
|
||||
() => eventCount.value != 0,
|
||||
);
|
||||
|
||||
function select() {
|
||||
selectedDate.value = parseInt(date);
|
||||
}
|
||||
|
||||
function drop() {
|
||||
handleDrop(date);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
class="tile"
|
||||
on:click={select}
|
||||
toggle:selected={isSelected}
|
||||
toggle:today={isToday}
|
||||
on:dragover={allowDrop}
|
||||
on:drop={drop}
|
||||
>
|
||||
<div>
|
||||
<b>{date}</b>
|
||||
<span
|
||||
class="event-count"
|
||||
toggle:has-events={hasEvents}
|
||||
subscribe:innerText={eventCount}
|
||||
></span>
|
||||
<div
|
||||
class="event-list"
|
||||
children:append={[mapState, converter]}
|
||||
></div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
currentWeekday++;
|
||||
if (currentWeekday == 7) currentWeekday = 0;
|
||||
}
|
||||
|
||||
const offsetElements: HTMLElement[] = [];
|
||||
for (let i = 0; i < monthGrid.offset; i++) {
|
||||
offsetElements.push(<div></div>);
|
||||
}
|
||||
|
||||
const converter: React.StateItemConverter<TaskViewModel> = (
|
||||
taskViewModel
|
||||
) => {
|
||||
const view = (
|
||||
<span class="ellipsis secondary">{taskViewModel.task.name}</span>
|
||||
);
|
||||
|
||||
taskViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
|
||||
return view;
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="month-grid-wrapper">
|
||||
<div class="day-labels">{...dayLabels}</div>
|
||||
<div class="month-grid">
|
||||
{...offsetElements}
|
||||
{...Object.entries(monthGrid.days)
|
||||
.sort((a, b) => localeCompare(a[0], b[0]))
|
||||
.map((entry) => {
|
||||
const [date, mapState] = entry;
|
||||
|
||||
const isSelected = React.createProxyState(
|
||||
[selectedDate],
|
||||
() => selectedDate.value == parseInt(date)
|
||||
);
|
||||
|
||||
const isToday: boolean =
|
||||
monthGrid.isCurrentMonth == true &&
|
||||
parseInt(date) == new Date().getDate();
|
||||
|
||||
const eventCount: React.State<number> = React.createProxyState(
|
||||
[mapState],
|
||||
() => mapState.value.size
|
||||
);
|
||||
|
||||
const hasEvents: React.State<boolean> = React.createProxyState(
|
||||
[eventCount],
|
||||
() => eventCount.value != 0
|
||||
);
|
||||
|
||||
function select() {
|
||||
selectedDate.value = parseInt(date);
|
||||
}
|
||||
|
||||
function drop() {
|
||||
handleDrop(date);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
class="tile"
|
||||
on:click={select}
|
||||
toggle:selected={isSelected}
|
||||
toggle:today={isToday}
|
||||
on:dragover={allowDrop}
|
||||
on:drop={drop}
|
||||
>
|
||||
<div>
|
||||
<b>{date}</b>
|
||||
<span
|
||||
class="event-count"
|
||||
toggle:has-events={hasEvents}
|
||||
subscribe:innerText={eventCount}
|
||||
></span>
|
||||
<div
|
||||
class="event-list"
|
||||
children:append={[mapState, converter]}
|
||||
></div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
export function Option(text: string, value: string, selectedOnCreate: boolean) {
|
||||
return (
|
||||
<option value={value} toggle:selected={selectedOnCreate}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
return (
|
||||
<option value={value} toggle:selected={selectedOnCreate}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
|
||||
export const StringToOption: React.StateItemConverter<string> = (
|
||||
string: string
|
||||
string: string,
|
||||
) => {
|
||||
return Option(string, string, false);
|
||||
return Option(string, string, false);
|
||||
};
|
||||
|
||||
export const EntryToOption: React.StateItemConverter<Entry> = (
|
||||
entry: Entry
|
||||
entry: Entry,
|
||||
) => {
|
||||
return Option(entry[1], entry[0], false);
|
||||
return Option(entry[1], entry[0], false);
|
||||
};
|
||||
|
||||
export const VersionIdToOption: React.StateItemConverter<string> = (
|
||||
versionId: string
|
||||
versionId: string,
|
||||
) => {
|
||||
const [date, rest] = versionId.split("T");
|
||||
const [time] = rest.split(".");
|
||||
const readableName = `${date} ${time}`;
|
||||
return Option(readableName, versionId, false);
|
||||
const [date, rest] = versionId.split("T");
|
||||
const [time] = rest.split(".");
|
||||
const readableName = `${date} ${time}`;
|
||||
return Option(readableName, versionId, false);
|
||||
};
|
||||
|
||||
export type Entry = [string, string];
|
||||
|
||||
@@ -1,70 +1,71 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import {
|
||||
StringEntryObject,
|
||||
collectObjectValuesForKey,
|
||||
localeCompare,
|
||||
StringEntryObject,
|
||||
collectObjectValuesForKey,
|
||||
localeCompare,
|
||||
} from "../../Model/Utility/utility";
|
||||
|
||||
export function PropertyValueList<T>(
|
||||
propertyKey: string,
|
||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||
objects: React.ListState<T> | React.MapState<T>,
|
||||
viewBuilder: (
|
||||
keys: React.ListState<string>,
|
||||
sortedKeys: React.State<string[]>
|
||||
) => HTMLElement
|
||||
propertyKey: string,
|
||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||
objects: React.ListState<T> | React.MapState<T>,
|
||||
viewBuilder: (
|
||||
keys: React.ListState<string>,
|
||||
sortedKeys: React.State<string[]>,
|
||||
) => HTMLElement,
|
||||
) {
|
||||
const propertyValues: React.ListState<string> = new React.ListState();
|
||||
const sortedPropertyValues: React.State<string[]> = createSortedPropertyValueState(propertyValues)
|
||||
objects.subscribe(() => {
|
||||
collectPropertyValuesToState(
|
||||
propertyKey,
|
||||
stringEntryObjectConverter,
|
||||
objects,
|
||||
propertyValues
|
||||
);
|
||||
});
|
||||
const propertyValues: React.ListState<string> = new React.ListState();
|
||||
const sortedPropertyValues: React.State<string[]> =
|
||||
createSortedPropertyValueState(propertyValues);
|
||||
objects.subscribe(() => {
|
||||
collectPropertyValuesToState(
|
||||
propertyKey,
|
||||
stringEntryObjectConverter,
|
||||
objects,
|
||||
propertyValues,
|
||||
);
|
||||
});
|
||||
|
||||
return viewBuilder(propertyValues, sortedPropertyValues);
|
||||
return viewBuilder(propertyValues, sortedPropertyValues);
|
||||
}
|
||||
|
||||
export function collectPropertyValuesToState<T>(
|
||||
propertyKey: string,
|
||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||
objects: React.ListState<T> | React.MapState<T>,
|
||||
propertyValues: React.ListState<string>
|
||||
propertyKey: string,
|
||||
stringEntryObjectConverter: (object: T) => StringEntryObject,
|
||||
objects: React.ListState<T> | React.MapState<T>,
|
||||
propertyValues: React.ListState<string>,
|
||||
) {
|
||||
const values: string[] = collectObjectValuesForKey(
|
||||
propertyKey,
|
||||
stringEntryObjectConverter,
|
||||
[...objects.value.values()]
|
||||
);
|
||||
for (const existingValue of values) {
|
||||
if (propertyValues.value.has(existingValue)) continue;
|
||||
propertyValues.add(existingValue);
|
||||
}
|
||||
|
||||
for (const displayedValue of propertyValues.value.values()) {
|
||||
if (values.includes(displayedValue) == false) {
|
||||
propertyValues.remove(displayedValue);
|
||||
const values: string[] = collectObjectValuesForKey(
|
||||
propertyKey,
|
||||
stringEntryObjectConverter,
|
||||
[...objects.value.values()],
|
||||
);
|
||||
for (const existingValue of values) {
|
||||
if (propertyValues.value.has(existingValue)) continue;
|
||||
propertyValues.add(existingValue);
|
||||
}
|
||||
|
||||
for (const displayedValue of propertyValues.value.values()) {
|
||||
if (values.includes(displayedValue) == false) {
|
||||
propertyValues.remove(displayedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function createSortedPropertyValueState(
|
||||
propertyValues: React.ListState<string>
|
||||
propertyValues: React.ListState<string>,
|
||||
): React.State<string[]> {
|
||||
return React.createProxyState([propertyValues], () =>
|
||||
[...propertyValues.value.values()].sort(localeCompare)
|
||||
);
|
||||
return React.createProxyState([propertyValues], () =>
|
||||
[...propertyValues.value.values()].sort(localeCompare),
|
||||
);
|
||||
}
|
||||
|
||||
export function createPropertyValueIndexState(
|
||||
sortedKeys: React.State<string[]>,
|
||||
key: string
|
||||
sortedKeys: React.State<string[]>,
|
||||
key: string,
|
||||
): React.State<number> {
|
||||
return React.createProxyState([sortedKeys], () =>
|
||||
sortedKeys.value.indexOf(key)
|
||||
);
|
||||
return React.createProxyState([sortedKeys], () =>
|
||||
sortedKeys.value.indexOf(key),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
import BoardViewModel, {
|
||||
BoardPageType,
|
||||
BoardPageType,
|
||||
} from "../../ViewModel/Pages/boardViewModel";
|
||||
|
||||
export function RibbonButton(
|
||||
label: string,
|
||||
icon: string,
|
||||
isSelected: React.State<boolean>,
|
||||
select: () => void
|
||||
label: string,
|
||||
icon: string,
|
||||
isSelected: React.State<boolean>,
|
||||
select: () => void,
|
||||
) {
|
||||
return (
|
||||
<button
|
||||
class="ribbon-button"
|
||||
aria-label={label}
|
||||
toggle:selected={isSelected}
|
||||
on:click={select}
|
||||
>
|
||||
<span class="icon">{icon}</span>
|
||||
</button>
|
||||
);
|
||||
return (
|
||||
<button
|
||||
class="ribbon-button"
|
||||
aria-label={label}
|
||||
toggle:selected={isSelected}
|
||||
on:click={select}
|
||||
>
|
||||
<span class="icon">{icon}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,55 +3,58 @@ import * as React from "bloatless-react";
|
||||
import TaskViewModel from "../../ViewModel/Pages/taskViewModel";
|
||||
|
||||
export function TaskEntry(taskViewModel: TaskViewModel) {
|
||||
const details: { [icon: string]: string } = {
|
||||
description: taskViewModel.description.value || "---",
|
||||
priority_high: taskViewModel.priority.value || "---",
|
||||
category: taskViewModel.category.value || "---",
|
||||
clock_loader_40: taskViewModel.status.value || "---",
|
||||
calendar_month: taskViewModel.date.value || "---",
|
||||
schedule: taskViewModel.time.value || "---",
|
||||
};
|
||||
const details: { [icon: string]: string } = {
|
||||
description: taskViewModel.description.value || "---",
|
||||
priority_high: taskViewModel.priority.value || "---",
|
||||
category: taskViewModel.category.value || "---",
|
||||
clock_loader_40: taskViewModel.status.value || "---",
|
||||
calendar_month: taskViewModel.date.value || "---",
|
||||
schedule: taskViewModel.time.value || "---",
|
||||
};
|
||||
|
||||
const view = (
|
||||
<button
|
||||
draggable="true"
|
||||
class="tile flex-no"
|
||||
style="user-select: none; -webkit-user-select: none"
|
||||
on:click={taskViewModel.open}
|
||||
on:dragstart={taskViewModel.dragStart}
|
||||
>
|
||||
<div>
|
||||
<b class="ellipsis" subscribe:innerText={taskViewModel.name}></b>
|
||||
<hr></hr>
|
||||
<div
|
||||
class="grid secondary"
|
||||
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
|
||||
const view = (
|
||||
<button
|
||||
draggable="true"
|
||||
class="tile flex-no"
|
||||
style="user-select: none; -webkit-user-select: none"
|
||||
on:click={taskViewModel.open}
|
||||
on:dragstart={taskViewModel.dragStart}
|
||||
>
|
||||
{...Object.entries(details).map((entry) => (
|
||||
<span
|
||||
class="flex-row align-center width-100 flex-no clip"
|
||||
style="gap: 1rem"
|
||||
>
|
||||
<span class="icon" style="font-size: 1.1rem">
|
||||
{entry[0]}
|
||||
</span>
|
||||
<span class="ellipsis">{entry[1]}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
<div>
|
||||
<b
|
||||
class="ellipsis"
|
||||
subscribe:innerText={taskViewModel.name}
|
||||
></b>
|
||||
<hr></hr>
|
||||
<div
|
||||
class="grid secondary"
|
||||
style="grid-template-columns: repeat(2, 1fr); column-gap: 1rem; row-gap: .5rem"
|
||||
>
|
||||
{...Object.entries(details).map((entry) => (
|
||||
<span
|
||||
class="flex-row align-center width-100 flex-no clip"
|
||||
style="gap: 1rem"
|
||||
>
|
||||
<span class="icon" style="font-size: 1.1rem">
|
||||
{entry[0]}
|
||||
</span>
|
||||
<span class="ellipsis">{entry[1]}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
taskViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
taskViewModel.index.subscribe((newIndex) => {
|
||||
view.style.order = newIndex;
|
||||
});
|
||||
|
||||
return view;
|
||||
return view;
|
||||
}
|
||||
|
||||
export const TaskViewModelToEntry: React.StateItemConverter<TaskViewModel> = (
|
||||
taskViewModel: TaskViewModel
|
||||
taskViewModel: TaskViewModel,
|
||||
) => {
|
||||
return TaskEntry(taskViewModel);
|
||||
return TaskEntry(taskViewModel);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "bloatless-react";
|
||||
|
||||
export const StringToTextSpan: React.StateItemConverter<string> = (
|
||||
string: string
|
||||
string: string,
|
||||
) => {
|
||||
return <span class="ellipsis">{string}</span>
|
||||
return <span class="ellipsis">{string}</span>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user