react-beautiful-dnd#DropResult TypeScript Examples

The following examples show how to use react-beautiful-dnd#DropResult. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: BuildOrder.tsx    From sc2-planner with MIT License 6 votes vote down vote up
onDragEnd(result: DropResult): void {
        // Dropped outside the list
        if (!result.destination) {
            return
        }

        const items: Array<IBuildOrderElement> = reorder(
            this.props.gamelogic.bo,
            result.source.index,
            result.destination.index
        )

        this.props.rerunBuildOrder(items)

        this.props.updateUrl(
            this.props.gamelogic.race,
            items,
            this.props.gamelogic.exportSettings(),
            this.props.gamelogic.exportOptimizeSettings()
        )
    }
Example #2
Source File: DndContextProvider.tsx    From calories-in with MIT License 6 votes vote down vote up
function DndContextProvider({ children }: Props) {
  const dietFormActions = useDietFormActions()

  const onDragEnd = (dropResult: DropResult) => {
    const { source, destination, type } = dropResult

    if (!destination) {
      return
    }

    if (type === 'variantsList') {
      dietFormActions.moveVariantForm(source.index, destination.index)
    } else if (type === 'mealsList') {
      dietFormActions.moveMealForm(source.index, destination.index)
    } else if (type === 'ingredientsList') {
      dietFormActions.moveIngredientForm(
        source.droppableId,
        source.index,
        destination.droppableId,
        destination.index
      )
    }
  }

  return <DragDropContext onDragEnd={onDragEnd}>{children}</DragDropContext>
}
Example #3
Source File: Tree.tsx    From react-beautiful-tree with Apache License 2.0 6 votes vote down vote up
onDragEnd = (result: DropResult) => {
    const { onDragEnd, tree } = this.props
    const { flattenedTree } = this.state
    this.expandTimer.stop()

    const finalDragState: DragState = {
      ...this.dragState!,
      source: result.source,
      destination: result.destination,
      combine: result.combine,
    }

    this.setState({
      draggedItemId: undefined,
    })

    const { sourcePosition, destinationPosition } = calculateFinalDropPositions(
      tree,
      flattenedTree,
      finalDragState
    )

    onDragEnd(sourcePosition, destinationPosition)

    this.dragState = undefined
  }
Example #4
Source File: FlowList.tsx    From Protoman with MIT License 6 votes vote down vote up
FlowList: React.FunctionComponent<Props> = ({ collectionName }) => {
  const dispatch = useDispatch();

  const collection = useSelector((s: AppState) => getByKey(s.collections, collectionName));
  const flowNames = useSelector((s: AppState) => collection?.flows?.map(([n]) => n));
  const isCurrentCollection = useSelector((s: AppState) => s.currentCollection === collectionName);
  const currentFlow = useSelector((s: AppState) => s.currentFlow);

  function handleSelection(flowName: string): void {
    dispatch(selectFlow(collectionName, flowName));
  }

  function validateFlowName(flowName: string): boolean {
    return !collection?.flows?.map(([n]) => n)?.includes(flowName);
  }

  function handleDelete(flowName: string): void {
    const flowCount = collection?.flows?.length || 0;
    if (flowCount > 1) {
      dispatch(deleteFlow(collectionName, flowName));
    } else {
      message.error("Can't delete the last request");
    }
  }

  function handleClone(originalFlowName: string): void {
    //check if this clone already exists
    const tmpName = originalFlowName.concat('_clone');
    let tmpNameIdx = 1;
    while (!validateFlowName(`${tmpName}${tmpNameIdx}`)) tmpNameIdx++;
    dispatch(cloneFlow(collectionName, originalFlowName, `${tmpName}${tmpNameIdx}`));
  }

  function handleDragEnd(result: DropResult): void {
    console.log(result);
    if (!result.destination || result.source.droppableId != result.destination.droppableId) return;

    const src = result.source.index;
    const dst = result.destination.index;

    dispatch(reorderFlow(collectionName, src, dst));
  }

  return (
    <DragDropContext onDragEnd={handleDragEnd}>
      <Droppable droppableId={collectionName}>
        {(provided): React.ReactElement => (
          <div {...provided.droppableProps} ref={provided.innerRef}>
            <List
              dataSource={flowNames}
              rowKey={(name): string => name}
              renderItem={(flowName, idx): React.ReactNode => (
                <FlowCell
                  idx={idx}
                  flowName={flowName}
                  emphasize={isCurrentCollection && currentFlow === flowName}
                  handleSelection={handleSelection}
                  handleDelete={handleDelete}
                  handleClone={handleClone}
                />
              )}
            />
            {provided.placeholder}
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}
Example #5
Source File: AccountManagementPage.tsx    From signer with Apache License 2.0 6 votes vote down vote up
onDragEnd = (result: DropResult) => {
    // dropped outside the list
    if (!result.destination) {
      return;
    }

    this.props.accountManager.reorderAccount(
      result.source.index,
      result.destination.index
    );
  };
Example #6
Source File: TasksBoard.tsx    From projectboard with MIT License 5 votes vote down vote up
TasksBoard = () => {
    const backlogTasks: Array<Task> = useSelector((state: RootState) => state.taskList?.tasks.backlog);
    const todoTasks: Array<Task> = useSelector((state: RootState) => state.taskList?.tasks.todo);
    const inProgressTasks: Array<Task> = useSelector(
        (state: RootState) => state.taskList?.tasks?.in_progress
    );
    const doneTasks: Array<Task> = useSelector((state: RootState) => state.taskList?.tasks?.done);
    const canceledTasks: Array<Task> = useSelector((state: RootState) => state.taskList?.tasks?.cancelled);

    const todoSorted: Array<Task> = todoTasks.sort((prev: Task, next: Task) => prev.order - next.order);
    const backlogSorted: Array<Task> = backlogTasks.sort((prev: Task, next: Task) => prev.order - next.order);
    const inProgressSorted: Array<Task> = inProgressTasks.sort(
        (prev: Task, next: Task) => prev.order - next.order
    );
    const doneSorted: Array<Task> = doneTasks.sort((prev: Task, next: Task) => prev.order - next.order);
    const cancelledSorted: Array<Task> = canceledTasks.sort(
        (prev: Task, next: Task) => prev.order - next.order
    );

    const match = useRouteMatch<MatchParams>();
    const { getAccessTokenSilently } = useAuth0();

    // dispatch
    const dispatch = useDispatch<AppDispatch>();
    const onDragEnd = async (
        { source, destination, draggableId }: DropResult,
        provided: ResponderProvided
    ) => {
        if (source.droppableId === destination?.droppableId) return;
        if (!source || !destination) return;
        const token = await getAccessTokenSilently();
        dispatch(
            changeStatusOfTaskBoard(
                draggableId,
                source.droppableId,
                destination.droppableId,
                source.index,
                destination.index,
                match.params.projectId,
                token
            )
        );
    };

    return (
        <DragDropContext onDragEnd={onDragEnd}>
            <div className="flex flex-1 pt-6 pl-8 overflow-scroll bg-gray-100">
                <IssueCol title={'Backlog'} status={Status.BACKLOG} tasks={backlogSorted} />
                <IssueCol title={'Todo'} status={Status.TODO} tasks={todoSorted} />
                <IssueCol title={'In Progress'} status={Status.IN_PROGRESS} tasks={inProgressSorted} />
                <IssueCol title={'Done'} status={Status.DONE} tasks={doneSorted} />
                <IssueCol title={'Canceled'} status={Status.CANCELED} tasks={cancelledSorted} />
            </div>
        </DragDropContext>
    );
}
Example #7
Source File: Todos.tsx    From max-todos with MIT License 5 votes vote down vote up
Todos = () => {
  const { todos, moveTodo } = useContext(MainContext)!;
  const [deleteSnackOpen, setDeleteSnackOpen] = useState(false);
  const [editSnackOpen, setEditSnackOpen] = useState(false);
  const [dragging, setDragging] = useState(false);
  const onDragEnd = (x: DropResult) => {
    if (!x.destination) return console.log(x);
    moveTodo(x.source.index, x.destination.index);
    setTimeout(() => setDragging(false), 200);
  };
  return (
    <>
      <DragDropContext
        onBeforeDragStart={() => setDragging(true)}
        onDragEnd={onDragEnd}
      >
        <Droppable droppableId="0">
          {(p) => (
            <div {...p.droppableProps} ref={p.innerRef}>
              <FlipMove disableAllAnimations={dragging}>
                {todos.map((todo, i) => {
                  return (
                    <Todo
                      todo={todo}
                      key={todo.id}
                      onDelete={() => setDeleteSnackOpen(true)}
                      index={i}
                      onEdit={() => setEditSnackOpen(true)}
                    />
                  );
                })}
              </FlipMove>
              {p.placeholder}
            </div>
          )}
        </Droppable>
      </DragDropContext>

      <Snackbar
        open={deleteSnackOpen}
        autoHideDuration={4000}
        onClose={() => setDeleteSnackOpen(false)}
        anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
      >
        <Alert
          elevation={6}
          variant="filled"
          onClose={() => setDeleteSnackOpen(false)}
          severity="success"
        >
          Successfully deleted item!
        </Alert>
      </Snackbar>
      <Snackbar
        open={editSnackOpen}
        autoHideDuration={4000}
        onClose={() => setEditSnackOpen(false)}
        anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
      >
        <Alert
          elevation={6}
          variant="filled"
          onClose={() => setEditSnackOpen(false)}
          severity="success"
        >
          Successfully edited item!
        </Alert>
      </Snackbar>
    </>
  );
}
Example #8
Source File: useDnd.ts    From TabMerger with GNU General Public License v3.0 5 votes vote down vote up
export default function useDnd() {
  const dispatch = useDispatch();

  const [groupTitle] = useLocalStorage("groupTitle", DEFAULT_GROUP_TITLE);
  const [groupColor] = useLocalStorage("groupColor", DEFAULT_GROUP_COLOR);
  const [windowTitle] = useLocalStorage("windowTitle", DEFAULT_WINDOW_TITLE);

  const { active } = useSelector((state) => state.groups.present);

  const { index } = active;

  const onBeforeCapture = useCallback(
    ({ draggableId }: BeforeCapture) => {
      const windowDrag = isWindowDrag(draggableId);
      const tabDrag = isTabDrag(draggableId);

      if (windowDrag) {
        // Hide tabs during a window drag
        toggleWindowTabsVisibility(draggableId, false);
      } else if (tabDrag) {
        // Add window to end of group (only if not in the first group)
        index > 0 && dispatch(addWindow({ index, name: windowTitle }));
      }

      if (windowDrag || tabDrag) {
        // Add group to end of side panel on both tab and window drag types
        dispatch(addGroup({ title: groupTitle, color: groupColor }));
      }
    },
    [dispatch, index, groupTitle, groupColor, windowTitle]
  );

  const onDragStart = useCallback(
    ({ draggableId }: DragStart) => {
      dispatch(updateDragOriginType(draggableId));
      dispatch(updateIsDragging(true));
    },
    [dispatch]
  );

  const onDragEnd = useCallback(
    ({ source, destination, combine, draggableId }: DropResult) => {
      const [isTab, isWindow, isGroup] = [isTabDrag, isWindowDrag, isGroupDrag].map((cb) => cb(draggableId));
      const commonPayload = { index, source };
      const sidePanelPayload = { ...commonPayload, combine };
      const destPayload = { ...commonPayload, destination };

      const isValidCombine = combine && Number(combine.draggableId.split("-")[1]) > 0;
      const isValidDndWithinGroup = destination && destination.droppableId !== "sidePanel";

      if (isTab) {
        isValidCombine && dispatch(updateTabsFromSidePanelDnd({ ...sidePanelPayload, name: windowTitle }));
        isValidDndWithinGroup && dispatch(updateTabsFromGroupDnd(destPayload));
      } else if (isWindow) {
        // Re-show the tabs since the drag ended
        toggleWindowTabsVisibility(draggableId, true);

        isValidCombine && dispatch(updateWindowsFromSidePanelDnd(sidePanelPayload));
        isValidDndWithinGroup && dispatch(updateWindowsFromGroupDnd(destPayload));
      } else if (isGroup && destination && destination.index > 0) {
        // Only swap if the destination exists (valid) and is below the first group
        dispatch(updateGroupOrder({ source, destination }));
      }

      dispatch(resetDnDInfo());

      /**
       * Must clear the windows in the current group first, then clear the group
       * @note Only relevant for tab or window dragging since a group drag does not add either a (temporary) window or group
       */
      if (isTab || isWindow) {
        dispatch(clearEmptyWindows({ index }));
        dispatch(clearEmptyGroups(active));
      }
    },
    [dispatch, index, windowTitle, active]
  );

  return { onBeforeCapture, onDragStart, onDragEnd };
}
Example #9
Source File: onDragEnd.ts    From taskcafe with MIT License 5 votes vote down vote up
onDragEnd = (
  { draggableId, source, destination, type }: DropResult,
  task: Task,
  onChecklistDrop: OnChecklistDropFn,
  onChecklistItemDrop: OnChecklistItemDropFn,
) => {
  if (typeof destination === 'undefined') return;
  if (!isPositionChanged(source, destination)) return;

  const isChecklist = type === 'checklist';
  const isSameChecklist = destination.droppableId === source.droppableId;
  let droppedDraggable: DraggableElement | null = null;
  let beforeDropDraggables: Array<DraggableElement> | null = null;

  if (!task.checklists) return;
  if (isChecklist) {
    const droppedGroup = task.checklists.find(taskGroup => taskGroup.id === draggableId);
    if (droppedGroup) {
      droppedDraggable = {
        id: draggableId,
        position: droppedGroup.position,
      };
      beforeDropDraggables = getSortedDraggables(
        task.checklists.map(checklist => {
          return { id: checklist.id, position: checklist.position };
        }),
      );
      if (droppedDraggable === null || beforeDropDraggables === null) {
        throw new Error('before drop draggables is null');
      }
      const afterDropDraggables = getAfterDropDraggableList(
        beforeDropDraggables,
        droppedDraggable,
        isChecklist,
        isSameChecklist,
        destination,
      );
      const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
      onChecklistDrop({ ...droppedGroup, position: newPosition });
    } else {
      throw new Error('task group can not be found');
    }
  } else {
    const targetChecklist = task.checklists.findIndex(
      checklist => checklist.items.findIndex(item => item.id === draggableId) !== -1,
    );
    const droppedChecklistItem = task.checklists[targetChecklist].items.find(item => item.id === draggableId);

    if (droppedChecklistItem) {
      droppedDraggable = {
        id: draggableId,
        position: droppedChecklistItem.position,
      };
      beforeDropDraggables = getSortedDraggables(
        task.checklists[targetChecklist].items.map(item => {
          return { id: item.id, position: item.position };
        }),
      );
      if (droppedDraggable === null || beforeDropDraggables === null) {
        throw new Error('before drop draggables is null');
      }
      const afterDropDraggables = getAfterDropDraggableList(
        beforeDropDraggables,
        droppedDraggable,
        isChecklist,
        isSameChecklist,
        destination,
      );
      const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
      const newItem = {
        ...droppedChecklistItem,
        position: newPosition,
      };
      onChecklistItemDrop(droppedChecklistItem.taskChecklistID, destination.droppableId, newItem);
    }
  }
}
Example #10
Source File: BookmarksTable.tsx    From flame with MIT License 4 votes vote down vote up
BookmarksTable = ({ openFormForUpdating }: Props): JSX.Element => {
  const {
    bookmarks: { categoryInEdit },
    config: { config },
  } = useSelector((state: State) => state);

  const dispatch = useDispatch();
  const {
    deleteBookmark,
    updateBookmark,
    createNotification,
    reorderBookmarks,
  } = bindActionCreators(actionCreators, dispatch);

  const [localBookmarks, setLocalBookmarks] = useState<Bookmark[]>([]);

  // Copy bookmarks array
  useEffect(() => {
    if (categoryInEdit) {
      setLocalBookmarks([...categoryInEdit.bookmarks]);
    }
  }, [categoryInEdit]);

  // Drag and drop handler
  const dragEndHanlder = (result: DropResult): void => {
    if (config.useOrdering !== 'orderId') {
      createNotification({
        title: 'Error',
        message: 'Custom order is disabled',
      });
      return;
    }

    if (!result.destination) {
      return;
    }

    const tmpBookmarks = [...localBookmarks];
    const [movedBookmark] = tmpBookmarks.splice(result.source.index, 1);
    tmpBookmarks.splice(result.destination.index, 0, movedBookmark);

    setLocalBookmarks(tmpBookmarks);

    const categoryId = categoryInEdit?.id || -1;
    reorderBookmarks(tmpBookmarks, categoryId);
  };

  // Action hanlders
  const deleteBookmarkHandler = (id: number, name: string) => {
    const categoryId = categoryInEdit?.id || -1;

    const proceed = window.confirm(`Are you sure you want to delete ${name}?`);
    if (proceed) {
      deleteBookmark(id, categoryId);
    }
  };

  const updateBookmarkHandler = (id: number) => {
    const bookmark =
      categoryInEdit?.bookmarks.find((b) => b.id === id) || bookmarkTemplate;

    openFormForUpdating(bookmark);
  };

  const changeBookmarkVisibiltyHandler = (id: number) => {
    const bookmark =
      categoryInEdit?.bookmarks.find((b) => b.id === id) || bookmarkTemplate;

    const categoryId = categoryInEdit?.id || -1;
    const [prev, curr] = [categoryId, categoryId];

    updateBookmark(
      id,
      { ...bookmark, isPublic: !bookmark.isPublic },
      { prev, curr }
    );
  };

  return (
    <Fragment>
      {!categoryInEdit ? (
        <Message isPrimary={false}>
          Switch to grid view and click on the name of category you want to edit
        </Message>
      ) : (
        <Message isPrimary={false}>
          Editing bookmarks from&nbsp;<span>{categoryInEdit.name}</span>
          &nbsp;category
        </Message>
      )}

      {categoryInEdit && (
        <DragDropContext onDragEnd={dragEndHanlder}>
          <Droppable droppableId="bookmarks">
            {(provided) => (
              <Table
                headers={[
                  'Name',
                  'URL',
                  'Icon',
                  'Visibility',
                  'Category',
                  'Actions',
                ]}
                innerRef={provided.innerRef}
              >
                {localBookmarks.map((bookmark, index): JSX.Element => {
                  return (
                    <Draggable
                      key={bookmark.id}
                      draggableId={bookmark.id.toString()}
                      index={index}
                    >
                      {(provided, snapshot) => {
                        const style = {
                          border: snapshot.isDragging
                            ? '1px solid var(--color-accent)'
                            : 'none',
                          borderRadius: '4px',
                          ...provided.draggableProps.style,
                        };

                        return (
                          <tr
                            {...provided.draggableProps}
                            {...provided.dragHandleProps}
                            ref={provided.innerRef}
                            style={style}
                          >
                            <td style={{ width: '200px' }}>{bookmark.name}</td>
                            <td style={{ width: '200px' }}>{bookmark.url}</td>
                            <td style={{ width: '200px' }}>{bookmark.icon}</td>
                            <td style={{ width: '200px' }}>
                              {bookmark.isPublic ? 'Visible' : 'Hidden'}
                            </td>
                            <td style={{ width: '200px' }}>
                              {categoryInEdit.name}
                            </td>

                            {!snapshot.isDragging && (
                              <TableActions
                                entity={bookmark}
                                deleteHandler={deleteBookmarkHandler}
                                updateHandler={updateBookmarkHandler}
                                changeVisibilty={changeBookmarkVisibiltyHandler}
                                showPin={false}
                              />
                            )}
                          </tr>
                        );
                      }}
                    </Draggable>
                  );
                })}
              </Table>
            )}
          </Droppable>
        </DragDropContext>
      )}
    </Fragment>
  );
}
Example #11
Source File: Step1Columns.tsx    From firetable with Apache License 2.0 4 votes vote down vote up
export default function Step1Columns({ config, setConfig }: IStepProps) {
  const classes = useStyles();

  // Get a list of fields from first 50 documents
  const { tableState } = useFiretableContext();
  const allFields = useMemo(() => {
    const sample = tableState!.rows.slice(0, 50);
    const fields_ = new Set<string>();
    sample.forEach((doc) =>
      Object.keys(doc).forEach((key) => {
        if (key !== "ref") fields_.add(key);
      })
    );
    return Array.from(fields_).sort();
  }, [tableState?.rows]);

  // Store selected fields
  const [selectedFields, setSelectedFields] = useState<string[]>(
    _sortBy(Object.keys(config), "index")
  );

  const handleSelect = (field: string) => (_, checked: boolean) => {
    if (checked) {
      setSelectedFields([...selectedFields, field]);
    } else {
      const newSelection = [...selectedFields];
      newSelection.splice(newSelection.indexOf(field), 1);
      setSelectedFields(newSelection);
    }
  };

  const handleSelectAll = () => {
    if (selectedFields.length !== allFields.length)
      setSelectedFields(allFields);
    else setSelectedFields([]);
  };

  const handleDragEnd = (result: DropResult) => {
    const newOrder = [...selectedFields];
    const [removed] = newOrder.splice(result.source.index, 1);
    newOrder.splice(result.destination!.index, 0, removed);
    setSelectedFields(newOrder);
  };

  useEffect(() => {
    setConfig(
      selectedFields.reduce(
        (a, c, i) => ({
          ...a,
          [c]: {
            fieldName: c,
            key: c,
            name: config[c]?.name || _startCase(c),
            type:
              config[c]?.type ||
              suggestType(tableState!.rows, c) ||
              FieldType.shortText,
            index: i,
            config: {},
          },
        }),
        {}
      )
    );
  }, [selectedFields]);

  return (
    <Grid container spacing={3}>
      <Grid item xs={12} sm={6}>
        <Typography variant="overline" gutterBottom component="h2">
          Select Columns ({selectedFields.length} of {allFields.length})
        </Typography>
        <Divider />

        <FadeList>
          <li>
            <FormControlLabel
              control={
                <Checkbox
                  checked={selectedFields.length === allFields.length}
                  indeterminate={
                    selectedFields.length !== 0 &&
                    selectedFields.length !== allFields.length
                  }
                  onChange={handleSelectAll}
                  color="default"
                />
              }
              label={
                <Typography variant="subtitle2" color="textSecondary">
                  Select all
                </Typography>
              }
              classes={{
                root: classes.formControlLabel,
                label: classes.columnLabel,
              }}
            />
          </li>
          <li className={classes.spacer} />

          {allFields.map((field) => (
            <li key={field}>
              <FormControlLabel
                key={field}
                control={
                  <Checkbox
                    checked={selectedFields.indexOf(field) > -1}
                    aria-label={`Select column ${field}`}
                    onChange={handleSelect(field)}
                    color="default"
                  />
                }
                label={<Column label={field} />}
                classes={{
                  root: classes.formControlLabel,
                  label: classes.columnLabel,
                }}
              />
            </li>
          ))}
        </FadeList>
      </Grid>
      <Grid item xs={12} sm={6}>
        <Typography variant="overline" gutterBottom component="h2">
          Sort Firetable Columns
        </Typography>
        <Divider />

        {selectedFields.length === 0 ? (
          <FadeList>
            <EmptyState Icon={AddColumnIcon} message="No columns selected" />
          </FadeList>
        ) : (
          <DragDropContext onDragEnd={handleDragEnd}>
            <FadeList>
              <Droppable droppableId="droppable">
                {(provided) => (
                  <div {...provided.droppableProps} ref={provided.innerRef}>
                    {selectedFields.map((field, i) => (
                      <li key={field}>
                        <Draggable draggableId={field} index={i}>
                          {(provided, snapshot) => (
                            <div
                              ref={provided.innerRef}
                              {...provided.draggableProps}
                              {...provided.dragHandleProps}
                            >
                              <Column
                                label={field}
                                active={snapshot.isDragging}
                                secondaryItem={<DragHandleIcon />}
                              />
                            </div>
                          )}
                        </Draggable>
                      </li>
                    ))}
                    {provided.placeholder}
                  </div>
                )}
              </Droppable>
            </FadeList>
          </DragDropContext>
        )}
      </Grid>
    </Grid>
  );
}
Example #12
Source File: AccountManagementPage.tsx    From clarity with Apache License 2.0 4 votes vote down vote up
AccountManagementPage = observer((props: Props) => {
  const [openDialog, setOpenDialog] = React.useState(false);
  const [openKeyDialog, setOpenKeyDialog] = React.useState(false);
  const [
    selectedAccount,
    setSelectedAccount
  ] = React.useState<SignKeyPairWithAlias | null>(null);
  const [name, setName] = React.useState('');
  const [publicKey64, setPublicKey64] = React.useState('');
  const [publicKeyHex, setPublicKeyHex] = React.useState('');
  /* Note: 01 prefix denotes algorithm used in key generation */
  const address = '01' + publicKeyHex;
  const [copyStatus, setCopyStatus] = React.useState(false);

  const handleClickOpen = (account: SignKeyPairWithAlias) => {
    setOpenDialog(true);
    setSelectedAccount(account);
    setName(account.name);
  };

  const handleViewKey = async (accountName: string) => {
    let publicKey64 = await props.authContainer.getSelectedAccountKey(
      accountName
    );
    let publicKeyHex = await props.authContainer.getPublicKeyHex(accountName);
    setName(accountName);
    setPublicKey64(publicKey64);
    setPublicKeyHex(publicKeyHex);
    setOpenKeyDialog(true);
  };

  const handleCopyMessage = (event?: React.SyntheticEvent, reason?: string) => {
    if (reason === 'clickaway') {
      return;
    }
    setCopyStatus(false);
  };

  const handleClose = () => {
    setOpenDialog(false);
    setOpenKeyDialog(false);
    setSelectedAccount(null);
  };

  const handleUpdateName = () => {
    if (selectedAccount) {
      props.authContainer.renameUserAccount(selectedAccount.name, name);
      handleClose();
    }
  };

  const onDragEnd = (result: DropResult) => {
    // dropped outside the list
    if (!result.destination) {
      return;
    }

    props.authContainer.reorderAccount(
      result.source.index,
      result.destination.index
    );
  };

  const handleClickRemove = (name: string) => {
    confirm(
      <div className="text-danger">Remove account</div>,
      'Are you sure you want to remove this account?'
    ).then(() => props.authContainer.removeUserAccount(name));
  };

  return (
    <React.Fragment>
      <DragDropContext onDragEnd={result => onDragEnd(result)}>
        <Droppable droppableId="droppable">
          {(provided, snapshot) => (
            <Observer>
              {() => (
                <RootRef rootRef={provided.innerRef}>
                  <List>
                    {props.authContainer.userAccounts.map((item, index) => (
                      <Draggable
                        key={item.name}
                        draggableId={item.name}
                        index={index}
                      >
                        {(provided, snapshot) => (
                          <ListItem
                            innerRef={provided.innerRef}
                            ContainerProps={{
                              ...provided.draggableProps,
                              ...provided.dragHandleProps,
                              style: getItemStyle(
                                snapshot.isDragging,
                                provided.draggableProps.style
                              )
                            }}
                          >
                            <ListItemText primary={item.name} />
                            <ListItemSecondaryAction>
                              <IconButton
                                edge={'end'}
                                onClick={() => {
                                  handleClickOpen(item);
                                }}
                              >
                                <EditIcon />
                              </IconButton>
                              <IconButton
                                edge={'end'}
                                onClick={() => {
                                  handleClickRemove(item.name);
                                }}
                              >
                                <DeleteIcon />
                              </IconButton>
                              <IconButton
                                edge={'end'}
                                onClick={() => {
                                  handleViewKey(item.name);
                                }}
                              >
                                <VpnKeyIcon />
                              </IconButton>
                            </ListItemSecondaryAction>
                          </ListItem>
                        )}
                      </Draggable>
                    ))}
                    {provided.placeholder}
                  </List>
                </RootRef>
              )}
            </Observer>
          )}
        </Droppable>
      </DragDropContext>
      <Dialog
        open={openDialog}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        <DialogTitle id="form-dialog-title">Rename</DialogTitle>
        <DialogContent>
          <Input
            autoFocus
            margin="dense"
            id="name"
            type="text"
            fullWidth
            value={name}
            onChange={e => {
              setName(e.target.value);
            }}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Cancel
          </Button>
          <Button onClick={handleUpdateName} color="primary">
            Update
          </Button>
        </DialogActions>
      </Dialog>

      <Dialog
        fullScreen
        open={openKeyDialog}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        <DialogTitle id="form-dialog-title">Account Details</DialogTitle>
        <DialogContent>
          <List>
            <ListSubheader>
              <Typography variant={'h6'}>{name}</Typography>
            </ListSubheader>
            <ListItem>
              <IconButton
                edge={'start'}
                onClick={() => {
                  copy(address);
                  setCopyStatus(true);
                }}
              >
                <FilterNoneIcon />
              </IconButton>
              <ListItemText
                primary={'Address: ' + address}
                style={{ overflowWrap: 'break-word' }}
              />
            </ListItem>
            <ListItem>
              <IconButton
                edge={'start'}
                onClick={() => {
                  copy(publicKey64);
                  setCopyStatus(true);
                }}
              >
                <FilterNoneIcon />
              </IconButton>
              <ListItemText
                primary={'Public Key: ' + publicKey64}
                style={{ overflowWrap: 'break-word' }}
              />
            </ListItem>
          </List>
          <Snackbar
            open={copyStatus}
            message="Copied!"
            autoHideDuration={1500}
            onClose={handleCopyMessage}
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Close
          </Button>
        </DialogActions>
      </Dialog>
    </React.Fragment>
  );
})
Example #13
Source File: board.tsx    From wikitrivia with MIT License 4 votes vote down vote up
export default function Board(props: Props) {
  const { highscore, resetGame, state, setState, updateHighscore } = props;

  const [isDragging, setIsDragging] = React.useState(false);

  async function onDragStart() {
    setIsDragging(true);
    navigator.vibrate(20);
  }

  async function onDragEnd(result: DropResult) {
    setIsDragging(false);

    const { source, destination } = result;

    if (
      !destination ||
      state.next === null ||
      (source.droppableId === "next" && destination.droppableId === "next")
    ) {
      return;
    }

    const item = { ...state.next };

    if (source.droppableId === "next" && destination.droppableId === "played") {
      const newDeck = [...state.deck];
      const newPlayed = [...state.played];
      const { correct, delta } = checkCorrect(
        newPlayed,
        item,
        destination.index
      );
      newPlayed.splice(destination.index, 0, {
        ...state.next,
        played: { correct },
      });

      const newNext = state.nextButOne;
      const newNextButOne = getRandomItem(
        newDeck,
        newNext ? [...newPlayed, newNext] : newPlayed
      );
      const newImageCache = [preloadImage(newNextButOne.image)];

      setState({
        ...state,
        deck: newDeck,
        imageCache: newImageCache,
        next: newNext,
        nextButOne: newNextButOne,
        played: newPlayed,
        lives: correct ? state.lives : state.lives - 1,
        badlyPlaced: correct
          ? null
          : {
              index: destination.index,
              rendered: false,
              delta,
            },
      });
    } else if (
      source.droppableId === "played" &&
      destination.droppableId === "played"
    ) {
      const newPlayed = [...state.played];
      const [item] = newPlayed.splice(source.index, 1);
      newPlayed.splice(destination.index, 0, item);

      setState({
        ...state,
        played: newPlayed,
        badlyPlaced: null,
      });
    }
  }

  // Ensure that newly placed items are rendered as draggables before trying to
  // move them to the right place if needed.
  React.useLayoutEffect(() => {
    if (
      state.badlyPlaced &&
      state.badlyPlaced.index !== null &&
      !state.badlyPlaced.rendered
    ) {
      setState({
        ...state,
        badlyPlaced: { ...state.badlyPlaced, rendered: true },
      });
    }
  }, [setState, state]);

  const score = React.useMemo(() => {
    return state.played.filter((item) => item.played.correct).length - 1;
  }, [state.played]);

  React.useLayoutEffect(() => {
    if (score > highscore) {
      updateHighscore(score);
    }
  }, [score, highscore, updateHighscore]);

  return (
    <DragDropContext
      onDragEnd={onDragEnd}
      onDragStart={onDragStart}
      sensors={[useAutoMoveSensor.bind(null, state)]}
    >
      <div className={styles.wrapper}>
        <div className={styles.top}>
          <Hearts lives={state.lives} />
          {state.lives > 0 ? (
            <>
              <NextItemList next={state.next} />
            </>
          ) : (
            <GameOver
              highscore={highscore}
              resetGame={resetGame}
              score={score}
            />
          )}
        </div>
        <div id="bottom" className={styles.bottom}>
          <PlayedItemList
            badlyPlacedIndex={
              state.badlyPlaced === null ? null : state.badlyPlaced.index
            }
            isDragging={isDragging}
            items={state.played}
          />
        </div>
      </div>
    </DragDropContext>
  );
}
Example #14
Source File: modal-entry.tsx    From keycaplendar with MIT License 4 votes vote down vote up
ModalEntry = ({
  keyset: propsKeyset,
  loading,
  onClose,
  onSubmit,
  open,
  user,
}: ModalEntryProps) => {
  const device = useAppSelector(selectDevice);

  const allDesigners = useAppSelector(selectAllDesigners);
  const allProfiles = useAppSelector(selectAllProfiles);
  const allVendors = useAppSelector(selectAllVendors);
  const allVendorRegions = useAppSelector(selectAllVendorRegions);

  const [keyset, updateKeyset] = useImmer<KeysetState>(
    partialSet({ alias: nanoid(10) })
  );

  const keyedKeysetUpdate =
    <K extends keyof KeysetState>(key: K, payload: KeysetState[K]) =>
    (draft: KeysetState) => {
      draft[key] = payload;
    };

  const [salesImageLoaded, setSalesImageLoaded] = useState(false);

  const [focused, setFocused] = useState("");

  useEffect(() => {
    if (!user.isEditor && user.isDesigner) {
      updateKeyset(keyedKeysetUpdate("designer", [user.nickname]));
    }
    if (!open) {
      updateKeyset(partialSet({ alias: nanoid(10) }));
      setSalesImageLoaded(false);
      setFocused("");
    }
  }, [open]);

  useEffect(() => {
    if (propsKeyset) {
      updateKeyset(
        produce(propsKeyset, (draft) => {
          draft.alias ??= nanoid(10);
          draft.gbMonth ??= false;
          draft.notes ??= "";
          draft.sales ??= {
            img: "",
            thirdParty: false,
          };
          draft.sales.img ??= "";
          draft.sales.thirdParty ??= false;
          draft.vendors ??= [];
          draft.vendors.forEach((vendor) => {
            vendor.id ??= nanoid();
          });
          if (draft.gbMonth && draft.gbLaunch.length === 10) {
            draft.gbLaunch = draft.gbLaunch.slice(0, 7);
          }
        })
      );
    }
  }, [propsKeyset, open]);

  const setImage = (image: Blob | File) =>
    updateKeyset(keyedKeysetUpdate("image", image));

  const handleFocus = (e: FocusEvent<HTMLInputElement>) =>
    setFocused(e.target.name);

  const handleBlur = () => setFocused("");

  const toggleDate = () =>
    updateKeyset((keyset) => {
      keyset.gbMonth = !keyset.gbMonth;
    });

  const selectValue = (prop: string, value: string) => {
    if (hasKey(keyset, prop)) {
      updateKeyset(keyedKeysetUpdate(prop, value));
    }
    setFocused("");
  };

  const selectValueAppend = (prop: string, value: string) =>
    updateKeyset((draft) => {
      if (hasKey(draft, prop)) {
        const { [prop]: original } = draft;
        if (original) {
          if (is<string[]>(original)) {
            original[original.length - 1] = value;
            setFocused("");
          } else if (is<string>(original)) {
            const array = original.split(", ");
            array[array.length - 1] = value;
            draft[prop as KeysMatching<KeysetState, string>] = array.join(", ");
            setFocused("");
          }
        }
      }
    });

  const selectVendor = (prop: string, value: string) => {
    const property = prop.replace(/\d/g, "");
    const index = parseInt(prop.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (hasKey(vendor, property)) {
        vendor[property] = value;
        setFocused("");
      }
    });
  };

  const selectVendorAppend = (prop: string, value: string) => {
    const property = prop.replace(/\d/g, "");
    const index = parseInt(prop.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (hasKey(vendor, property)) {
        const { [property]: original } = vendor;
        if (typeof original !== "undefined") {
          const array = original.split(", ");
          array[array.length - 1] = value;
          vendor[property] = array.join(", ");
          setFocused("");
        }
      }
    });
  };

  const handleChange = ({
    target: { checked, name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    if (name === "designer") {
      updateKeyset(keyedKeysetUpdate(name, value.split(", ")));
    } else if (name === "shipped") {
      updateKeyset(keyedKeysetUpdate(name, checked));
    } else if (hasKey(keyset, name)) {
      updateKeyset(keyedKeysetUpdate(name, value));
    }
  };

  const handleSalesImage = ({
    target: { checked, name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    if (hasKey(keyset.sales, name)) {
      if (name === "thirdParty") {
        updateKeyset((keyset) => {
          keyset.sales[name] = checked;
        });
      } else {
        updateKeyset((keyset) => {
          keyset.sales[name] = value;
        });
      }
    }
  };

  const handleNamedChange =
    <Key extends keyof KeysetState>(name: Key) =>
    (value: KeysetState[Key]) =>
      updateKeyset(keyedKeysetUpdate(name, value));

  const handleChangeVendor = ({
    target: { name, value },
  }: ChangeEvent<HTMLInputElement>) => {
    const property = name.replace(/\d/g, "");
    const index = parseInt(name.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (hasKey(vendor, property)) {
        vendor[property] = value;
      }
    });
  };

  const handleNamedChangeVendor =
    (name: keyof VendorType, index: number) => (value: string) =>
      updateKeyset((draft) => {
        const {
          vendors: { [index]: vendor },
        } = draft;
        if (hasKey(vendor, name)) {
          vendor[name] = value;
        }
      });

  const handleChangeVendorEndDate = (e: ChangeEvent<HTMLInputElement>) => {
    const index = parseInt(e.target.name.replace(/\D/g, ""));
    updateKeyset((draft) => {
      const {
        vendors: { [index]: vendor },
      } = draft;
      if (e.target.checked) {
        vendor.endDate = "";
      } else {
        delete vendor.endDate;
      }
    });
  };

  const addVendor = () => {
    const emptyVendor = {
      id: nanoid(),
      name: "",
      region: "",
      storeLink: "",
    };
    updateKeyset((draft) => {
      draft.vendors.push(emptyVendor);
    });
  };

  const removeVendor = (index: number) =>
    updateKeyset((draft) => {
      draft.vendors.splice(index, 1);
    });

  const handleDragVendor = (result: DropResult) => {
    if (!result.destination) return;
    updateKeyset((draft) => {
      arrayMove(
        draft.vendors,
        result.source.index,
        result.destination?.index || 0
      );
    });
  };

  const result = useMemo(
    () =>
      gbMonthCheck(
        SetSchema.extend({
          id: z.string().min(propsKeyset ? 1 : 0),
          image: z.union([z.string().url(), z.instanceof(Blob)]),
        })
      ).safeParse(keyset),
    [keyset]
  );

  const useDrawer = device !== "mobile";
  const dateCard = keyset.gbMonth ? (
    <Card className="date-container" outlined>
      <Typography className="date-title" tag="h3" use="caption">
        Month
      </Typography>
      <div className="date-form">
        <DatePicker
          allowQuarter
          autoComplete="off"
          icon={iconObject(<CalendarToday />)}
          label="GB month"
          month
          name="gbLaunch"
          onChange={handleNamedChange("gbLaunch")}
          outlined
          showNowButton
          value={keyset.gbLaunch}
        />
      </div>
      <CardActions>
        <CardActionButtons>
          <CardActionButton label="Date" onClick={toggleDate} type="button" />
        </CardActionButtons>
      </CardActions>
    </Card>
  ) : (
    <Card className="date-container" outlined>
      <Typography className="date-title" tag="h3" use="caption">
        Date
      </Typography>
      <div className="date-form">
        <DatePicker
          allowQuarter
          autoComplete="off"
          icon={iconObject(<CalendarToday />)}
          label="GB launch"
          name="gbLaunch"
          onChange={handleNamedChange("gbLaunch")}
          outlined
          showNowButton
          value={keyset.gbLaunch}
        />
        <DatePicker
          autoComplete="off"
          fallbackValue={keyset.gbLaunch}
          icon={iconObject(<CalendarToday />)}
          label="GB end"
          name="gbEnd"
          onChange={handleNamedChange("gbEnd")}
          outlined
          showNowButton
          value={keyset.gbEnd}
        />
      </div>
      <CardActions>
        <CardActionButtons>
          <CardActionButton label="Month" onClick={toggleDate} type="button" />
        </CardActionButtons>
      </CardActions>
    </Card>
  );
  return (
    <BoolWrapper
      condition={useDrawer}
      falseWrapper={(children) => (
        <FullScreenDialog className="entry-modal" {...{ onClose, open }}>
          {children}
        </FullScreenDialog>
      )}
      trueWrapper={(children) => (
        <Drawer
          className="drawer-right entry-modal"
          modal
          {...{ onClose, open }}
        >
          {children}
        </Drawer>
      )}
    >
      <BoolWrapper
        condition={useDrawer}
        falseWrapper={(children) => (
          <FullScreenDialogAppBar>
            <TopAppBarRow>{children}</TopAppBarRow>
            <LinearProgress
              closed={!loading}
              progress={typeof loading === "number" ? loading : undefined}
            />
          </FullScreenDialogAppBar>
        )}
        trueWrapper={(children) => (
          <DrawerHeader>
            {children}
            <LinearProgress
              closed={!loading}
              progress={typeof loading === "number" ? loading : undefined}
            />
          </DrawerHeader>
        )}
      >
        <BoolWrapper
          condition={useDrawer}
          falseWrapper={(children) => (
            <TopAppBarSection alignStart>
              <TopAppBarNavigationIcon icon="close" onClick={onClose} />
              <TopAppBarTitle>{children}</TopAppBarTitle>
            </TopAppBarSection>
          )}
          trueWrapper={(children) => <DrawerTitle>{children}</DrawerTitle>}
        >
          {propsKeyset ? "Edit" : "Create"} Entry
        </BoolWrapper>

        <ConditionalWrapper
          condition={!useDrawer}
          wrapper={(children) => (
            <TopAppBarSection alignEnd>{children}</TopAppBarSection>
          )}
        >
          <Button
            disabled={!result?.success || !!loading}
            label="Save"
            onClick={() => onSubmit(keyset)}
            outlined={useDrawer}
            type="button"
          />
        </ConditionalWrapper>
      </BoolWrapper>
      <BoolWrapper
        condition={useDrawer}
        falseWrapper={(children) => (
          <FullScreenDialogContent>{children}</FullScreenDialogContent>
        )}
        trueWrapper={(children) => <DrawerContent>{children}</DrawerContent>}
      >
        <div className="banner">
          <div className="banner-text">Make sure to read the entry guide.</div>
          <div className="banner-button">
            <a
              href="/guides?guideId=JLB4xxfx52NJmmnbvbzO"
              rel="noopener noreferrer"
              target="_blank"
            >
              <Button label="guide" />
            </a>
          </div>
        </div>
        <form className="form">
          <div className="form-double">
            <div className="select-container">
              <MenuSurfaceAnchor>
                <TextField
                  autoComplete="off"
                  label="Profile"
                  name="profile"
                  onBlur={handleBlur}
                  onChange={handleChange}
                  onFocus={handleFocus}
                  outlined
                  required
                  value={keyset.profile}
                />
                <Autocomplete
                  array={allProfiles}
                  minChars={1}
                  open={focused === "profile"}
                  prop="profile"
                  query={keyset.profile}
                  select={selectValue}
                />
              </MenuSurfaceAnchor>
            </div>
            <div className="field-container">
              <TextField
                autoComplete="off"
                className="field"
                label="Colorway"
                name="colorway"
                onBlur={handleBlur}
                onChange={handleChange}
                onFocus={handleFocus}
                outlined
                required
                value={keyset.colorway}
              />
            </div>
          </div>
          <MenuSurfaceAnchor>
            <TextField
              autoComplete="off"
              disabled={user.isEditor === false && user.isDesigner}
              helpText={{
                children: (
                  <>
                    Separate multiple designers with{" "}
                    <code className="multiline">, </code>.
                  </>
                ),
                persistent: true,
              }}
              label="Designer"
              name="designer"
              onBlur={handleBlur}
              onChange={handleChange}
              onFocus={handleFocus}
              outlined
              required
              value={keyset.designer.join(", ")}
            />
            <Autocomplete
              array={allDesigners}
              listSplit
              minChars={2}
              open={focused === "designer"}
              prop="designer"
              query={keyset.designer.join(", ")}
              select={selectValueAppend}
            />
          </MenuSurfaceAnchor>
          <DatePicker
            autoComplete="off"
            icon={iconObject(<CalendarToday />)}
            label="IC date"
            name="icDate"
            onChange={handleNamedChange("icDate")}
            outlined
            pickerProps={{ disableFuture: true }}
            required
            showNowButton
            value={keyset.icDate}
          />
          <TextField
            autoComplete="off"
            helpText={{
              children: "Must be valid link",
              persistent: false,
              validationMsg: true,
            }}
            icon="link"
            label="Details"
            name="details"
            onBlur={handleBlur}
            onChange={handleChange}
            onFocus={handleFocus}
            outlined
            pattern={validLink}
            required
            value={keyset.details}
          />
          <TextField
            autoComplete="off"
            label="Notes"
            name="notes"
            onBlur={handleBlur}
            onChange={handleChange}
            onFocus={handleFocus}
            outlined
            rows={2}
            textarea
            value={keyset.notes}
          />
          <ImageUpload
            desktop={device === "desktop"}
            image={keyset.image}
            setImage={setImage}
          />
          {dateCard}
          <Checkbox
            checked={keyset.shipped}
            id="create-shipped"
            label="Shipped"
            name="shipped"
            onChange={handleChange}
          />
          <Typography className="subheader" tag="h3" use="caption">
            Vendors
          </Typography>
          <DragDropContext onDragEnd={handleDragVendor}>
            <Droppable droppableId="vendors-create">
              {(provided) => (
                <div
                  ref={provided.innerRef}
                  className="vendors-container"
                  {...provided.droppableProps}
                >
                  {keyset.vendors.map((vendor, index) => {
                    const endDateField =
                      typeof vendor.endDate === "string" ? (
                        <DatePicker
                          autoComplete="off"
                          icon={iconObject(<CalendarToday />)}
                          label="End date"
                          name={`endDate${index}`}
                          onChange={handleNamedChangeVendor("endDate", index)}
                          outlined
                          required
                          showNowButton
                          value={vendor.endDate}
                        />
                      ) : null;
                    return (
                      <Draggable
                        key={vendor.id}
                        draggableId={vendor.id ?? index.toString()}
                        index={index}
                      >
                        {(provided, snapshot) => (
                          <Card
                            ref={provided.innerRef}
                            className={classNames("vendor-container", {
                              dragged: snapshot.isDragging,
                            })}
                            outlined
                            {...provided.draggableProps}
                            style={getVendorStyle(provided)}
                          >
                            <div className="title-container">
                              <Typography
                                className="vendor-title"
                                use="caption"
                              >
                                {`Vendor ${index + 1}`}
                              </Typography>
                              {withTooltip(
                                <IconButton
                                  icon={iconObject(<Delete />)}
                                  onClick={() => {
                                    removeVendor(index);
                                  }}
                                  type="button"
                                />,
                                "Delete"
                              )}
                              {withTooltip(
                                <Icon
                                  className="drag-handle"
                                  icon="drag_handle"
                                  {...provided.dragHandleProps}
                                />,
                                "Drag"
                              )}
                            </div>
                            <div className="vendor-form">
                              <MenuSurfaceAnchor>
                                <TextField
                                  autoComplete="off"
                                  icon={iconObject(<Store />)}
                                  label="Name"
                                  name={`name${index}`}
                                  onBlur={handleBlur}
                                  onChange={handleChangeVendor}
                                  onFocus={handleFocus}
                                  outlined
                                  required
                                  value={vendor.name}
                                />
                                <Autocomplete
                                  array={allVendors}
                                  minChars={1}
                                  open={focused === `name${index}`}
                                  prop={`name${index}`}
                                  query={vendor.name}
                                  select={selectVendor}
                                />
                              </MenuSurfaceAnchor>
                              <MenuSurfaceAnchor>
                                <TextField
                                  autoComplete="off"
                                  icon={iconObject(<Public />)}
                                  label="Region"
                                  name={`region${index}`}
                                  onBlur={handleBlur}
                                  onChange={handleChangeVendor}
                                  onFocus={handleFocus}
                                  outlined
                                  required
                                  value={vendor.region}
                                />
                                <Autocomplete
                                  array={allVendorRegions}
                                  listSplit
                                  minChars={1}
                                  open={focused === `region${index}`}
                                  prop={`region${index}`}
                                  query={vendor.region}
                                  select={selectVendorAppend}
                                />
                              </MenuSurfaceAnchor>
                              <TextField
                                autoComplete="off"
                                helpText={{
                                  children: "Must be valid link",
                                  persistent: false,
                                  validationMsg: true,
                                }}
                                icon="link"
                                label="Store link"
                                name={`storeLink${index}`}
                                onBlur={handleBlur}
                                onChange={handleChangeVendor}
                                onFocus={handleFocus}
                                outlined
                                pattern={validLink}
                                value={vendor.storeLink}
                              />
                              <Checkbox
                                checked={
                                  !!vendor.endDate || vendor.endDate === ""
                                }
                                className="end-date-field"
                                id={`editEndDate${index}`}
                                label="Different end date"
                                name={`endDate${index}`}
                                onChange={handleChangeVendorEndDate}
                              />
                              {endDateField}
                            </div>
                          </Card>
                        )}
                      </Draggable>
                    );
                  })}
                  {provided.placeholder}
                </div>
              )}
            </Droppable>
          </DragDropContext>
          <div className="add-button">
            <Button
              label="Add vendor"
              onClick={addVendor}
              outlined
              type="button"
            />
          </div>
          <Card className="sales-container" outlined>
            <Typography className="sales-title" tag="h3" use="caption">
              Sales
            </Typography>
            <div
              className={classNames("sales-image", {
                loaded: salesImageLoaded,
              })}
            >
              <div className="sales-image-icon">
                <Icon icon={iconObject(<AddPhotoAlternate />)} />
              </div>
              <img
                alt=""
                onError={() => {
                  setSalesImageLoaded(false);
                }}
                onLoad={() => {
                  setSalesImageLoaded(true);
                }}
                src={keyset.sales.img}
              />
            </div>
            <div className="sales-field">
              <TextField
                autoComplete="off"
                helpText={{
                  children: "Must be direct link to image",
                  persistent: true,
                  validationMsg: true,
                }}
                icon="link"
                label="URL"
                name="img"
                onBlur={handleBlur}
                onChange={handleSalesImage}
                onFocus={handleFocus}
                outlined
                pattern={validLink}
                value={keyset.sales.img}
              />
              <Checkbox
                checked={keyset.sales.thirdParty}
                className="sales-checkbox"
                label="Third party graph"
                name="thirdParty"
                onChange={handleSalesImage}
              />
            </div>
          </Card>
        </form>
      </BoolWrapper>
    </BoolWrapper>
  );
}
Example #15
Source File: MediaMenu.tsx    From sync-party with GNU General Public License v3.0 4 votes vote down vote up
export default function MediaMenu({
    socket,
    setPlayerFocused,
    emitPlayWish,
    freezeUiVisible,
    isPlaying,
    playerState
}: Props): JSX.Element {
    const party: ClientParty | null = useSelector(
        (state: RootAppState) => state.globalState.party
    );
    const playingItem: MediaItem | null = useSelector(
        (state: RootAppState) => state.globalState.playingItem
    );
    const uiVisible = useSelector(
        (state: RootAppState) => state.globalState.uiVisible
    );
    const uiFocused = useSelector(
        (state: RootAppState) => state.globalState.uiFocused
    );

    const [hoverTimestamp, setHoverTimestamp] = useState(0);
    const hoverTimestampRef = useRef(hoverTimestamp);
    hoverTimestampRef.current = hoverTimestamp;

    const [addMediaIsActive, setAddMediaIsActive] = useState(false);

    const [partyItemsSet, setPartyItemsSet] = useState<Set<string>>(new Set());

    const partyItemListRef = useRef<HTMLDivElement | null>(null);

    const dispatch = useDispatch();
    const { t } = useTranslation();

    // Collect items in active party in a set for faster checks
    useEffect(() => {
        if (party) {
            const itemsSet = new Set<string>();

            party.items.forEach((item) => {
                itemsSet.add(item.id);
            });

            setPartyItemsSet(itemsSet);
        }
    }, [party]);

    const handleRemoveItemFromParty = async (
        item: MediaItem
    ): Promise<void> => {
        if (socket && party) {
            if (
                party.items.length === 1 &&
                playerState.playingItem &&
                playerState.playingItem.id === item.id
            ) {
                dispatch(
                    setGlobalState({
                        errorMessage: t(`errors.removeLastItemError`)
                    })
                );

                return Promise.reject();
            }

            try {
                const response = await Axios.delete('/api/partyItems', {
                    data: { itemId: item.id, partyId: party.id },
                    ...axiosConfig()
                });

                if (response.data.success) {
                    const currentIndex = playerState.playlistIndex;

                    if (
                        playerState.playingItem &&
                        playerState.playingItem.id === item.id
                    ) {
                        emitPlayWish(
                            party.items[
                                currentIndex < party.items.length - 1
                                    ? currentIndex + 1
                                    : currentIndex - 1
                            ],
                            playerState.isPlaying,
                            null,
                            false,
                            0
                        );
                    }

                    socket.emit('partyUpdate', {
                        partyId: party.id
                    });

                    setPlayerFocused(true);
                } else {
                    dispatch(
                        setGlobalState({
                            errorMessage: t(
                                `apiResponseMessages.${response.data.msg}`
                            )
                        })
                    );
                }
            } catch (error) {
                dispatch(
                    setGlobalState({
                        errorMessage: t(`errors.removeItemError`)
                    })
                );
            }
        }
    };

    const handleItemEditSave = async (item: MediaItem): Promise<void> => {
        if (socket && party) {
            try {
                const response = await Axios.put(
                    '/api/mediaItem/' + item.id,
                    item,
                    axiosConfig()
                );

                if (response.data.success) {
                    await getUpdatedUserItems(dispatch, t);

                    socket.emit('partyUpdate', { partyId: party.id });
                    setPlayerFocused(true);
                } else {
                    dispatch(
                        setGlobalState({
                            errorMessage: t(
                                `apiResponseMessages.${response.data.msg}`
                            )
                        })
                    );
                }
            } catch (error) {
                dispatch(
                    setGlobalState({
                        errorMessage: t(`errors.itemSaveError`)
                    })
                );
            }
        }
    };

    const handleItemClick = (chosenItem: MediaItem): void => {
        if (party) {
            const newSource = party.items.filter((mediaItem: MediaItem) => {
                return mediaItem.id === chosenItem.id;
            })[0];

            emitPlayWish(
                newSource,
                true,
                playerState.playingItem ? playerState.playingItem.id : null,
                true,
                0
            );
        }
    };

    const onDragEnd = async (result: DropResult): Promise<void> => {
        if (socket && party) {
            // Dropped outside the list
            if (!result.destination) {
                return;
            }

            const orderedItems = reorderItems(
                party.items,
                result.source.index,
                result.destination.index
            );

            try {
                const response = await Axios.put(
                    '/api/partyItems',
                    { orderedItems: orderedItems, partyId: party.id },
                    axiosConfig()
                );

                if (response.data.success) {
                    socket.emit('partyUpdate', { partyId: party.id });
                } else {
                    dispatch(
                        setGlobalState({
                            errorMessage: t(
                                `apiResponseMessages.${response.data.msg}`
                            )
                        })
                    );
                }
            } catch (error) {
                dispatch(
                    setGlobalState({
                        errorMessage: t(`errors.reorderError`)
                    })
                );
            }
        }
    };

    return (
        <div
            className={
                'mediaMenu fixed top-0 right-0 flex flex-col mt-16 p-2 border border-gray-500 rounded m-2 shadow-md backgroundShade z-50' +
                (uiVisible || !playingItem || !playingItem.url ? '' : ' hidden')
            }
            onMouseOver={(): void => {
                const now = Date.now();

                if (hoverTimestampRef.current + 10000 < now) {
                    freezeUiVisible(true);
                    setHoverTimestamp(now);
                }
            }}
            onMouseLeave={(): void => {
                if (!uiFocused.chat && !addMediaIsActive) {
                    freezeUiVisible(false);
                    setHoverTimestamp(0);
                }
            }}
        >
            {party && party.items.length ? (
                <div className="partyItemList" ref={partyItemListRef}>
                    <DragDropContext onDragEnd={onDragEnd}>
                        <Droppable droppableId="droppable">
                            {(provided: DroppableProvided): JSX.Element => (
                                <div
                                    {...provided.droppableProps}
                                    ref={provided.innerRef}
                                >
                                    {party.items.map(
                                        (
                                            mediaItem: MediaItem,
                                            index: number
                                        ) => {
                                            const isCurrentlyPlayingItem =
                                                playingItem &&
                                                playingItem.id === mediaItem.id
                                                    ? true
                                                    : false;
                                            const alreadyPlayed =
                                                party.metadata.played &&
                                                party.metadata.played[
                                                    mediaItem.id
                                                ]
                                                    ? true
                                                    : false;

                                            return (
                                                <MediaMenuDraggable
                                                    key={mediaItem.id}
                                                    mediaItemId={mediaItem.id}
                                                    index={index}
                                                >
                                                    <ItemListed
                                                        item={mediaItem}
                                                        isPlaying={isPlaying}
                                                        isCurrentlyPlayingItem={
                                                            isCurrentlyPlayingItem
                                                        }
                                                        alreadyPlayed={
                                                            alreadyPlayed
                                                        }
                                                        nameEditingAllowed={
                                                            false
                                                        }
                                                        handleItemClick={(): void =>
                                                            handleItemClick(
                                                                mediaItem
                                                            )
                                                        }
                                                        onRemoveButtonClick={(
                                                            item: MediaItem
                                                        ): void => {
                                                            handleRemoveItemFromParty(
                                                                item
                                                            );
                                                        }}
                                                        handleItemSave={(
                                                            item: MediaItem
                                                        ): void => {
                                                            handleItemEditSave(
                                                                item
                                                            );
                                                        }}
                                                        setPlayerFocused={
                                                            setPlayerFocused
                                                        }
                                                        partyItemListRef={
                                                            partyItemListRef
                                                        }
                                                    ></ItemListed>
                                                </MediaMenuDraggable>
                                            );
                                        }
                                    )}
                                </div>
                            )}
                        </Droppable>
                    </DragDropContext>
                </div>
            ) : (
                <div className="mb-2">There&#39;s nothing.</div>
            )}
            <AddMedia
                isActive={addMediaIsActive}
                partyItemsSet={partyItemsSet}
                setAddMediaIsActive={setAddMediaIsActive}
                handleItemEditSave={handleItemEditSave}
                setPlayerFocused={(focused: boolean): void =>
                    setPlayerFocused(focused)
                }
                socket={socket}
            ></AddMedia>
        </div>
    );
}
Example #16
Source File: index.tsx    From taskcafe with MIT License 4 votes vote down vote up
SimpleLists: React.FC<SimpleProps> = ({
  taskGroups,
  onTaskDrop,
  onChangeTaskGroupName,
  onCardLabelClick,
  onTaskGroupDrop,
  onTaskClick,
  onCreateTask,
  onQuickEditorOpen,
  onCreateTaskGroup,
  cardLabelVariant,
  onExtraMenuOpen,
  onCardMemberClick,
  taskStatusFilter = initTaskStatusFilter,
  isPublic = false,
  taskMetaFilters = initTaskMetaFilters,
  taskSorting = initTaskSorting,
}) => {
  const onDragEnd = ({ draggableId, source, destination, type }: DropResult) => {
    if (typeof destination === 'undefined') return;
    if (!isPositionChanged(source, destination)) return;

    const isList = type === 'column';
    const isSameList = destination.droppableId === source.droppableId;
    let droppedDraggable: DraggableElement | null = null;
    let beforeDropDraggables: Array<DraggableElement> | null = null;

    if (isList) {
      const droppedGroup = taskGroups.find((taskGroup) => taskGroup.id === draggableId);
      if (droppedGroup) {
        droppedDraggable = {
          id: draggableId,
          position: droppedGroup.position,
        };
        beforeDropDraggables = getSortedDraggables(
          taskGroups.map((taskGroup) => {
            return { id: taskGroup.id, position: taskGroup.position };
          }),
        );
        if (droppedDraggable === null || beforeDropDraggables === null) {
          throw new Error('before drop draggables is null');
        }
        const afterDropDraggables = getAfterDropDraggableList(
          beforeDropDraggables,
          droppedDraggable,
          isList,
          isSameList,
          destination,
        );
        const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
        onTaskGroupDrop({
          ...droppedGroup,
          position: newPosition,
        });
      } else {
        throw new Error('task group can not be found');
      }
    } else {
      const curTaskGroup = taskGroups.findIndex(
        (taskGroup) => taskGroup.tasks.findIndex((task) => task.id === draggableId) !== -1,
      );
      let targetTaskGroup = curTaskGroup;
      if (!isSameList) {
        targetTaskGroup = taskGroups.findIndex((taskGroup) => taskGroup.id === destination.droppableId);
      }
      const droppedTask = taskGroups[curTaskGroup].tasks.find((task) => task.id === draggableId);

      if (droppedTask) {
        droppedDraggable = {
          id: draggableId,
          position: droppedTask.position,
        };
        beforeDropDraggables = getSortedDraggables(
          taskGroups[targetTaskGroup].tasks.map((task) => {
            return { id: task.id, position: task.position };
          }),
        );
        if (droppedDraggable === null || beforeDropDraggables === null) {
          throw new Error('before drop draggables is null');
        }
        const afterDropDraggables = getAfterDropDraggableList(
          beforeDropDraggables,
          droppedDraggable,
          isList,
          isSameList,
          destination,
        );
        const newPosition = getNewDraggablePosition(afterDropDraggables, destination.index);
        const newTask = {
          ...droppedTask,
          position: newPosition,
          taskGroup: {
            id: destination.droppableId,
          },
        };
        log.debug(
          `action=move taskId=${droppedTask.id} source=${source.droppableId} dest=${destination.droppableId} oldPos=${droppedTask.position} newPos=${newPosition}`,
        );
        onTaskDrop(newTask, droppedTask.taskGroup.id);
      }
    }
  };

  const [currentComposer, setCurrentComposer] = useState('');
  const [toggleLabels, setToggleLabels] = useState(false);
  const [toggleDirection, setToggleDirection] = useState<'shrink' | 'expand'>(
    cardLabelVariant === 'large' ? 'shrink' : 'expand',
  );

  return (
    <BoardContainer>
      <BoardWrapper>
        <DragDropContext onDragEnd={onDragEnd}>
          <Droppable direction="horizontal" type="column" droppableId="root">
            {(provided) => (
              <Container {...provided.droppableProps} ref={provided.innerRef}>
                {taskGroups
                  .slice()
                  .sort((a: any, b: any) => a.position - b.position)
                  .map((taskGroup: TaskGroup, index: number) => {
                    return (
                      <Draggable draggableId={taskGroup.id} key={taskGroup.id} index={index}>
                        {(columnDragProvided) => (
                          <Droppable type="tasks" droppableId={taskGroup.id}>
                            {(columnDropProvided, snapshot) => (
                              <List
                                name={taskGroup.name}
                                onOpenComposer={(id) => setCurrentComposer(id)}
                                isComposerOpen={currentComposer === taskGroup.id}
                                onSaveName={(name) => onChangeTaskGroupName(taskGroup.id, name)}
                                isPublic={isPublic}
                                ref={columnDragProvided.innerRef}
                                wrapperProps={columnDragProvided.draggableProps}
                                headerProps={columnDragProvided.dragHandleProps}
                                onExtraMenuOpen={onExtraMenuOpen}
                                id={taskGroup.id}
                                key={taskGroup.id}
                                index={index}
                              >
                                <ListCards ref={columnDropProvided.innerRef} {...columnDropProvided.droppableProps}>
                                  {taskGroup.tasks
                                    .slice()
                                    .filter((t) => shouldStatusFilter(t, taskStatusFilter))
                                    .filter((t) => shouldMetaFilter(t, taskMetaFilters))
                                    .sort((a: any, b: any) => a.position - b.position)
                                    .sort((a: any, b: any) => sortTasks(a, b, taskSorting))
                                    .map((task: Task, taskIndex: any) => {
                                      return (
                                        <Draggable
                                          key={task.id}
                                          draggableId={task.id}
                                          index={taskIndex}
                                          isDragDisabled={taskSorting.type !== TaskSortingType.NONE}
                                        >
                                          {(taskProvided) => {
                                            return (
                                              <Card
                                                toggleDirection={toggleDirection}
                                                toggleLabels={toggleLabels}
                                                isPublic={isPublic}
                                                labelVariant={cardLabelVariant}
                                                watched={task.watched}
                                                wrapperProps={{
                                                  ...taskProvided.draggableProps,
                                                  ...taskProvided.dragHandleProps,
                                                }}
                                                setToggleLabels={setToggleLabels}
                                                onCardLabelClick={() => {
                                                  setToggleLabels(true);
                                                  setToggleDirection(
                                                    cardLabelVariant === 'large' ? 'shrink' : 'expand',
                                                  );
                                                  if (onCardLabelClick) {
                                                    onCardLabelClick();
                                                  }
                                                }}
                                                ref={taskProvided.innerRef}
                                                taskID={task.id}
                                                complete={task.complete ?? false}
                                                taskGroupID={taskGroup.id}
                                                description=""
                                                labels={task.labels.map((label) => label.projectLabel)}
                                                dueDate={
                                                  task.dueDate.at
                                                    ? {
                                                        isPastDue: false,
                                                        formattedDate: dayjs(task.dueDate.at).format('MMM D, YYYY'),
                                                      }
                                                    : undefined
                                                }
                                                title={task.name}
                                                members={task.assigned}
                                                onClick={() => {
                                                  onTaskClick(task);
                                                }}
                                                checklists={task.badges && task.badges.checklist}
                                                comments={task.badges && task.badges.comments}
                                                onCardMemberClick={onCardMemberClick}
                                                onContextMenu={onQuickEditorOpen}
                                              />
                                            );
                                          }}
                                        </Draggable>
                                      );
                                    })}
                                  {columnDropProvided.placeholder}
                                  {currentComposer === taskGroup.id && (
                                    <CardComposer
                                      onClose={() => {
                                        setCurrentComposer('');
                                      }}
                                      onCreateCard={(name) => {
                                        onCreateTask(taskGroup.id, name);
                                      }}
                                      isOpen
                                    />
                                  )}
                                </ListCards>
                              </List>
                            )}
                          </Droppable>
                        )}
                      </Draggable>
                    );
                  })}
                {provided.placeholder}
              </Container>
            )}
          </Droppable>
        </DragDropContext>
        {!isPublic && (
          <AddList
            onSave={(listName) => {
              onCreateTaskGroup(listName);
            }}
          />
        )}
      </BoardWrapper>
    </BoardContainer>
  );
}
Example #17
Source File: index.tsx    From nextjs-hasura-fullstack with MIT License 4 votes vote down vote up
BoardPage: React.FC = () => {
  const router = useRouter()
  const { setLastPosition } = useLastPositionNumber()

  const { data, loading } = useBoardQuery({
    variables: {
      id: Number(router.query.boardId),
    },
    skip: !router.query.boardId,
  })

  const [updateList] = useUpdateListMutation()
  const [updateCard] = useUpdateCardMutation()
  const [moveCard] = useMoveCardMutation()

  const renderLists = React.useMemo(() => {
    return data?.boards_by_pk?.lists || []
  }, [data])

  React.useEffect(() => {
    setLastPosition(renderLists[renderLists.length - 1]?.position || 1)
  }, [renderLists])

  const onDragEnd = async (result: DropResult, provided: ResponderProvided) => {
    const { source, destination, type } = result

    // dropped outside the list
    if (!destination) {
      return
    }

    if (
      source.droppableId === destination.droppableId &&
      source.index === destination.index
    ) {
      return
    }

    // DragDrop a "List"
    if (type === 'LIST') {
      const updatedPosition = getUpdatePositionReorder(
        renderLists,
        source.index,
        destination.index,
      )
      const sourceList = renderLists[source.index]

      updateList({
        variables: {
          id: sourceList.id,
          name: sourceList.name,
          position: updatedPosition,
        },
        update: (cache, { data }) => {
          if (data?.update_lists_by_pk) {
            const cacheId = cache.identify({
              __typename: 'boards',
              id: sourceList.board_id,
            })
            if (cacheId) {
              cache.modify({
                id: cacheId,
                fields: {
                  lists(existingListRefs = []) {
                    return reorder(
                      existingListRefs,
                      source.index,
                      destination.index,
                    )
                  },
                },
              })
            }
          }
        },
        optimisticResponse: (variables) => {
          return {
            __typename: 'mutation_root',
            update_lists_by_pk: {
              ...sourceList,
              position: updatedPosition,
            },
          }
        },
      })
    }

    if (type === 'CARD') {
      /* same list: reorder card */
      if (source.droppableId === destination.droppableId) {
        const listCards = renderLists.find(
          (item) => item.id === Number(source.droppableId),
        )?.cards

        if (!listCards) {
          return
        }

        const updatedPosition = getUpdatePositionReorder(
          listCards,
          source.index,
          destination.index,
        )

        const sourceCard = listCards[source.index]

        await updateCard({
          variables: {
            id: sourceCard.id,
            title: sourceCard.title,
            position: updatedPosition,
          },
          update: (cache, { data }) => {
            if (data?.update_cards_by_pk) {
              const cacheId = cache.identify({
                __typename: 'lists',
                id: sourceCard.list_id,
              })
              if (cacheId) {
                cache.modify({
                  id: cacheId,
                  fields: {
                    cards(existingCardRefs = []) {
                      return reorder(
                        existingCardRefs,
                        source.index,
                        destination.index,
                      )
                    },
                  },
                })
              }
            }
          },
          optimisticResponse: (variables) => {
            return {
              __typename: 'mutation_root',
              update_cards_by_pk: {
                ...sourceCard,
                position: updatedPosition,
              },
            }
          },
        })
      } else {
        /**
         * Diferent list: move card
         */

        const sourceListCards = renderLists.find(
          (item) => item.id === Number(source.droppableId),
        )?.cards

        const destinationListCards = renderLists.find(
          (item) => item.id === Number(destination.droppableId),
        )?.cards

        if (!sourceListCards || !destinationListCards) {
          return
        }

        const updatedPosition = getUpdatePositionMove(
          sourceListCards,
          destinationListCards,
          source.index,
          destination.index,
        )

        const sourceCard = sourceListCards[source.index]

        await moveCard({
          variables: {
            id: sourceCard.id,
            list_id: Number(destination.droppableId),
            title: sourceCard.title,
            position: updatedPosition,
          },
          update: (cache, { data }) => {
            if (data?.update_cards_by_pk) {
              const cardCacheId = cache.identify(data.update_cards_by_pk)

              if (!cardCacheId) {
                return
              }

              const cacheId = cache.identify({
                __typename: 'lists',
                id: source.droppableId,
              })

              if (cacheId) {
                cache.modify({
                  id: cacheId,
                  fields: {
                    cards(existingRefs = []) {
                      const next = existingRefs.filter(
                        (listRef: { __ref: string }) =>
                          listRef.__ref !== cardCacheId,
                      )
                      return next
                    },
                  },
                })
              }

              const cacheIdDestination = cache.identify({
                __typename: 'lists',
                id: destination.droppableId,
              })

              if (cacheIdDestination) {
                cache.modify({
                  id: cacheIdDestination,
                  fields: {
                    cards(existingCardRefs = [], { toReference }) {
                      const moveRef = toReference(cardCacheId)
                      if (moveRef) {
                        const next = produce(
                          existingCardRefs as any[],
                          (draftState) => {
                            draftState.splice(destination.index, 0, moveRef)
                          },
                        )
                        return next
                      }
                    },
                  },
                })
              }
            }
          },
          optimisticResponse: () => ({
            __typename: 'mutation_root',
            update_cards_by_pk: {
              ...sourceCard,
              position: updatedPosition,
            },
          }),
        })
      }
    }
  }

  let listRender
  if (loading) {
    listRender = (
      <div className={`flex flex-no-wrap min-w-max-content`}>
        {[...Array(3)].map((item, idx) => (
          <div
            key={idx}
            className="flex flex-col max-w-sm p-4 mx-4 space-y-3 border border-gray-300 rounded-md shadow w-72"
          >
            <div className="flex space-x-4 animate-pulse">
              <div className="flex-1 py-1 space-y-4">
                <div className="w-3/4 h-4 bg-gray-400 rounded"></div>
                {/* <div className="space-y-2">
                  <div className="h-4 bg-gray-400 rounded"></div>
                  <div className="w-5/6 h-4 bg-gray-400 rounded"></div>
                </div> */}
              </div>
            </div>
            {[...Array(4)].map((item, index) => (
              <div
                key={index}
                className="flex flex-col max-w-sm p-4 border border-gray-300 rounded-md shadow"
              >
                <div className="flex space-x-4 animate-pulse">
                  <div className="flex-1 py-1 space-y-2">
                    <div className="h-4 bg-gray-400 rounded"></div>
                    <div className="w-5/6 h-4 bg-gray-400 rounded"></div>
                  </div>
                </div>
              </div>
            ))}
            <div className="flex flex-col max-w-sm border-t border-gray-300 rounded-md shadow">
              <div className="flex space-x-4 animate-pulse">
                <div className="flex-1 py-1 space-y-2">
                  <div className="h-4 bg-gray-400 rounded"></div>
                </div>
              </div>
            </div>
          </div>
        ))}
        {/*  */}
      </div>
    )
  } else {
    listRender = (
      <div className={`flex flex-no-wrap h-screen w-max-content`}>
        <DragDropContext
          onDragStart={(source) => {
            console.log(`?? [LOG]: source`, source)
          }}
          // onDragUpdate={onDragUpdate}
          onDragEnd={onDragEnd}
        >
          {/* List */}
          <Droppable
            droppableId="board"
            type="LIST"
            direction="horizontal"
            // ignoreContainerClipping={true}
          >
            {(
              { innerRef, droppableProps, placeholder },
              { isDraggingOver },
            ) => {
              return (
                <div
                  ref={innerRef}
                  {...droppableProps}
                  className={`inline-flex ${isDraggingOver && ``}`}
                  style={{
                    overflowAnchor: 'none',
                  }}
                >
                  {renderLists.map((list, index) => (
                    <ListBoard list={list} index={index} key={index} />
                  ))}
                  {placeholder}
                </div>
              )
            }}
          </Droppable>
        </DragDropContext>
        <div className={``}>
          <NewList lastId={renderLists[renderLists.length - 1]?.id || 0} />
        </div>
      </div>
    )
  }

  return (
    <div className={`h-screen space-x-2 -mt-14`}>
      <Scrollbar noScrollY className={``}>
        <div className={`pt-14`}>
          <div className={`mt-4 mb-8`}>
            <h2
              className={`text-3xl font-semibold text-center text-cool-gray-100`}
            >
              {data?.boards_by_pk?.icon} {data?.boards_by_pk?.name}
            </h2>
          </div>
          {listRender}
        </div>
      </Scrollbar>
    </div>
  )
}
Example #18
Source File: Board.tsx    From knboard with MIT License 4 votes vote down vote up
Board = () => {
  const detail = useSelector((state: RootState) => state.board.detail);
  const error = useSelector((state: RootState) => state.board.detailError);
  const columns = useSelector(columnSelectors.selectAll);
  const tasksByColumn = useSelector((state: RootState) => state.task.byColumn);
  const tasksById = useSelector((state: RootState) => state.task.byId);
  const dispatch = useDispatch();
  const { id } = useParams();

  React.useEffect(() => {
    if (id) {
      dispatch(fetchBoardById({ boardId: id }));
    }
  }, [id]);

  const onDragEnd = (result: DropResult) => {
    // dropped nowhere
    if (!result.destination) {
      return;
    }

    const source: DraggableLocation = result.source;
    const destination: DraggableLocation = result.destination;

    // did not move anywhere - can bail early
    if (
      source.droppableId === destination.droppableId &&
      source.index === destination.index
    ) {
      return;
    }

    // reordering column
    if (result.type === "COLUMN") {
      const newOrdered: IColumn[] = reorder(
        columns,
        source.index,
        destination.index
      );
      dispatch(updateColumns(newOrdered));
      return;
    }

    const data = reorderTasks({
      tasksByColumn,
      source,
      destination,
    });
    dispatch(updateTasksByColumn(data.tasksByColumn));
  };

  const detailDataExists = detail?.id.toString() === id;

  if (error) {
    return <PageError>{error}</PageError>;
  }

  if (!detailDataExists) {
    return <Spinner loading={!detailDataExists} />;
  }

  return (
    <>
      <SEO title={detail?.name} />
      {columns.length !== 0 ? (
        <BoardContainer data-testid="board-container">
          <ColumnsBlock>
            <DragDropContext onDragEnd={onDragEnd}>
              <Droppable
                droppableId="board"
                type="COLUMN"
                direction="horizontal"
              >
                {(provided: DroppableProvided) => (
                  <ColumnContainer
                    ref={provided.innerRef}
                    {...provided.droppableProps}
                  >
                    {columns.map((column: IColumn, index: number) => (
                      <Column
                        key={column.id}
                        id={column.id}
                        title={column.title}
                        index={index}
                        tasks={tasksByColumn[column.id].map(
                          (taskId) => tasksById[taskId]
                        )}
                      />
                    ))}
                    {provided.placeholder}
                  </ColumnContainer>
                )}
              </Droppable>
            </DragDropContext>
          </ColumnsBlock>
          <RightMargin />
        </BoardContainer>
      ) : (
        <EmptyBoard>This board is empty.</EmptyBoard>
      )}
    </>
  );
}
Example #19
Source File: index.tsx    From slice-machine with Apache License 2.0 4 votes vote down vote up
FieldZones: React.FunctionComponent<FieldZonesProps> = ({
  Model,
  store,
  variation,
}) => {
  const _onDeleteItem = (widgetArea: Models.WidgetsArea) => (key: string) => {
    store
      .variation(variation.id)
      .deleteWidgetMockConfig(Model.mockConfig, widgetArea, key);
    store.variation(variation.id).removeWidget(widgetArea, key);
  };

  const _getFieldMockConfig =
    (widgetArea: Models.WidgetsArea) =>
    ({ apiId }: { apiId: string }) => {
      // eslint-disable-next-line @typescript-eslint/no-unsafe-return
      return SliceMockConfig.getFieldMockConfig(
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument
        Model.mockConfig,
        variation.id,
        widgetArea,
        apiId
      );
    };

  const _onSave =
    (widgetArea: Models.WidgetsArea) =>
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    ({ apiId: previousKey, newKey, value, mockValue }: any) => {
      if (mockValue) {
        store
          .variation(variation.id)
          .updateWidgetMockConfig(
            Model.mockConfig,
            widgetArea,
            previousKey,
            newKey,
            mockValue
          );
      } else {
        store
          .variation(variation.id)
          .deleteWidgetMockConfig(Model.mockConfig, widgetArea, newKey);
      }
      store
        .variation(variation.id)
        .replaceWidget(widgetArea, previousKey, newKey, value);
    };

  const _onSaveNewField =
    (widgetArea: Models.WidgetsArea) =>
    ({ id, widgetTypeName }: { id: string; widgetTypeName: string }) => {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-expect-error
      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
      const widget = Widgets[widgetTypeName];
      if (!widget) {
        console.log(
          `Could not find widget with type name "${widgetTypeName}". Please contact us!`
        );
      }

      const friendlyName = createFriendlyFieldNameWithId(id);

      store
        .variation(variation.id)
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
        .addWidget(widgetArea, id, widget.create(friendlyName));
    };

  const _onDragEnd =
    (widgetArea: Models.WidgetsArea) => (result: DropResult) => {
      if (ensureDnDDestination(result)) {
        return;
      }
      store
        .variation(variation.id)
        .reorderWidget(
          widgetArea,
          result.source.index,
          result.destination && result.destination.index
        );
    };

  return (
    <>
      <Zone
        tabId={undefined}
        mockConfig={Model.mockConfig}
        title="Non-Repeatable Zone"
        dataTip={dataTipText}
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        fields={variation.primary}
        EditModal={EditModal}
        widgetsArray={sliceBuilderWidgetsArray}
        getFieldMockConfig={_getFieldMockConfig(WidgetsArea.Primary)}
        onDeleteItem={_onDeleteItem(WidgetsArea.Primary)}
        onSave={_onSave(WidgetsArea.Primary)}
        onSaveNewField={_onSaveNewField(WidgetsArea.Primary)}
        onDragEnd={_onDragEnd(WidgetsArea.Primary)}
        poolOfFieldsToCheck={variation.primary || []}
        renderHintBase={({ item }) =>
          // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
          `slice.primary${transformKeyAccessor(item.key)}`
        }
        renderFieldAccessor={(key) =>
          // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
          `slice.primary${transformKeyAccessor(key)}`
        }
      />
      <Box mt={4} />
      <Zone
        tabId={undefined}
        isRepeatable
        mockConfig={Model.mockConfig}
        title="Repeatable Zone"
        dataTip={dataTipText2}
        widgetsArray={sliceBuilderWidgetsArray}
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        fields={variation.items}
        EditModal={EditModal}
        getFieldMockConfig={_getFieldMockConfig(WidgetsArea.Items)}
        onDeleteItem={_onDeleteItem(WidgetsArea.Items)}
        onSave={_onSave(WidgetsArea.Items)}
        onSaveNewField={_onSaveNewField(WidgetsArea.Items)}
        onDragEnd={_onDragEnd(WidgetsArea.Items)}
        poolOfFieldsToCheck={variation.items || []}
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
        renderHintBase={({ item }) => `item${transformKeyAccessor(item.key)}`}
        renderFieldAccessor={(key) =>
          // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
          `slice.items[i]${transformKeyAccessor(key)}`
        }
      />
    </>
  );
}
Example #20
Source File: CategoryTable.tsx    From flame with MIT License 4 votes vote down vote up
CategoryTable = ({ openFormForUpdating }: Props): JSX.Element => {
  const {
    config: { config },
    bookmarks: { categories },
  } = useSelector((state: State) => state);

  const dispatch = useDispatch();
  const {
    pinCategory,
    deleteCategory,
    createNotification,
    reorderCategories,
    updateCategory,
  } = bindActionCreators(actionCreators, dispatch);

  const [localCategories, setLocalCategories] = useState<Category[]>([]);

  // Copy categories array
  useEffect(() => {
    setLocalCategories([...categories]);
  }, [categories]);

  // Drag and drop handler
  const dragEndHanlder = (result: DropResult): void => {
    if (config.useOrdering !== 'orderId') {
      createNotification({
        title: 'Error',
        message: 'Custom order is disabled',
      });
      return;
    }

    if (!result.destination) {
      return;
    }

    const tmpCategories = [...localCategories];
    const [movedCategory] = tmpCategories.splice(result.source.index, 1);
    tmpCategories.splice(result.destination.index, 0, movedCategory);

    setLocalCategories(tmpCategories);
    reorderCategories(tmpCategories);
  };

  // Action handlers
  const deleteCategoryHandler = (id: number, name: string) => {
    const proceed = window.confirm(
      `Are you sure you want to delete ${name}? It will delete ALL assigned bookmarks`
    );

    if (proceed) {
      deleteCategory(id);
    }
  };

  const updateCategoryHandler = (id: number) => {
    const category = categories.find((c) => c.id === id) as Category;
    openFormForUpdating(category);
  };

  const pinCategoryHandler = (id: number) => {
    const category = categories.find((c) => c.id === id) as Category;
    pinCategory(category);
  };

  const changeCategoryVisibiltyHandler = (id: number) => {
    const category = categories.find((c) => c.id === id) as Category;
    updateCategory(id, { ...category, isPublic: !category.isPublic });
  };

  return (
    <Fragment>
      <Message isPrimary={false}>
        {config.useOrdering === 'orderId' ? (
          <p>You can drag and drop single rows to reorder categories</p>
        ) : (
          <p>
            Custom order is disabled. You can change it in the{' '}
            <Link to="/settings/general">settings</Link>
          </p>
        )}
      </Message>

      <DragDropContext onDragEnd={dragEndHanlder}>
        <Droppable droppableId="categories">
          {(provided) => (
            <Table
              headers={['Name', 'Visibility', 'Actions']}
              innerRef={provided.innerRef}
            >
              {localCategories.map((category, index): JSX.Element => {
                return (
                  <Draggable
                    key={category.id}
                    draggableId={category.id.toString()}
                    index={index}
                  >
                    {(provided, snapshot) => {
                      const style = {
                        border: snapshot.isDragging
                          ? '1px solid var(--color-accent)'
                          : 'none',
                        borderRadius: '4px',
                        ...provided.draggableProps.style,
                      };

                      return (
                        <tr
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          ref={provided.innerRef}
                          style={style}
                        >
                          <td style={{ width: '300px' }}>{category.name}</td>
                          <td style={{ width: '300px' }}>
                            {category.isPublic ? 'Visible' : 'Hidden'}
                          </td>

                          {!snapshot.isDragging && (
                            <TableActions
                              entity={category}
                              deleteHandler={deleteCategoryHandler}
                              updateHandler={updateCategoryHandler}
                              pinHanlder={pinCategoryHandler}
                              changeVisibilty={changeCategoryVisibiltyHandler}
                            />
                          )}
                        </tr>
                      );
                    }}
                  </Draggable>
                );
              })}
            </Table>
          )}
        </Droppable>
      </DragDropContext>
    </Fragment>
  );
}
Example #21
Source File: EntitiesDiagram.tsx    From amplication with Apache License 2.0 4 votes vote down vote up
export default function EntitiesDiagram() {
  const [error, setError] = useState<Error | null>(null);

  const { setFieldValue, values } = useFormikContext<EntitiesDiagramFormData>();
  const [zoomLevel, setZoomLevel] = useState<number>(1);

  const zoomIn = useCallback(() => {
    setZoomLevel((zoomLevel) => {
      if (zoomLevel < ZOOM_LEVEL_MAX) return zoomLevel + ZOOM_LEVEL_STEP;
      return zoomLevel;
    });
  }, []);
  const zoomOut = useCallback(() => {
    setZoomLevel((zoomLevel) => {
      if (zoomLevel > ZOOM_LEVEL_MIN) return zoomLevel - ZOOM_LEVEL_STEP;
      return zoomLevel;
    });
  }, []);

  const zoomReset = useCallback(() => {
    setZoomLevel(1);
  }, []);

  const clearError = useCallback(() => {
    setError(null);
  }, [setError]);

  const [editedField, setEditedField] = React.useState<FieldIdentifier | null>(
    null
  );

  const [editedEntity, setEditedEntity] = React.useState<number | null>(0);

  const resetEditableElements = useCallback(() => {
    setEditedEntity(null);
    setEditedField(null);
    setError(null);
  }, [setEditedEntity, setEditedField]);

  const handleEditEntity = useCallback(
    (entityIndex: number | null) => {
      setEditedEntity(entityIndex);
      setEditedField(null);
    },
    [setEditedEntity, setEditedField]
  );

  const handleEditField = useCallback(
    (fieldIdentifier: FieldIdentifier | null) => {
      setEditedField(fieldIdentifier);
      setEditedEntity(null);
    },
    [setEditedEntity, setEditedField]
  );

  const onFieldDragStart = useCallback(() => {
    resetEditableElements();
  }, [resetEditableElements]);

  const onFieldDragEnd = useCallback(
    (result: DropResult) => {
      // dropped outside the list
      if (!result.destination) {
        return;
      }

      const [sourceEntityIndex, sourceFieldIndex] = result.draggableId.split(
        "_"
      );
      const [, destinationEntityIndex] = result.destination.droppableId.split(
        "_"
      );
      const destinationFieldIndex = result.destination.index;

      const sourceFields = values.entities[Number(sourceEntityIndex)].fields;
      const [sourceField] = sourceFields.splice(Number(sourceFieldIndex), 1);

      setFieldValue(`entities.${sourceEntityIndex}.fields`, [...sourceFields]);

      const destinationFields =
        values.entities[Number(destinationEntityIndex)].fields;

      destinationFields.splice(destinationFieldIndex, 0, sourceField);

      setFieldValue(`entities.${destinationEntityIndex}.fields`, [
        ...destinationFields,
      ]);

      resetEditableElements();
    },
    [values, setFieldValue, resetEditableElements]
  );

  const [entitiesPosition, setEntitiesPosition] = useState<
    EntitiesPositionData
  >({
    0: {
      top: 0,
      left: 0,
      width: DEFAULT_ENTITY_WIDTH,
      height: DEFAULT_ENTITY_HEIGHT,
    },
  });

  //used to force redraw the arrows (the internal lists of fields are not updated since it used  )
  const [, forceUpdate] = useReducer((x) => x + 1, 0);

  const handleEntityDrag = useCallback(
    (entityIndex: number, draggableData: DraggableData) => {
      resetEditableElements();
      setEntitiesPosition((current) => {
        const position = current[entityIndex] || { top: 0, left: 0 };
        const deltaX = draggableData.deltaX / zoomLevel;
        const deltaY = draggableData.deltaY / zoomLevel;

        const newPosition = {
          top: position.top + deltaY > 0 ? position.top + deltaY : 0,
          left: position.left + deltaX > 0 ? position.left + deltaX : 0,
          width: draggableData.node.offsetWidth,
          height: draggableData.node.offsetHeight,
        };

        current[entityIndex] = newPosition;

        return current;
      });

      forceUpdate();
    },
    [forceUpdate, resetEditableElements, zoomLevel]
  );

  const handleDeleteEntity = useCallback(
    (entityIndex: number) => {
      setError(null);

      const clonedEntities = cloneDeep(values.entities);
      const relations =
        clonedEntities[entityIndex].relationsToEntityIndex || [];
      const currentEntity = clonedEntities[entityIndex];

      const clonedPositions = cloneDeep(entitiesPosition);

      if (isEmpty(currentEntity.fields) && isEmpty(relations)) {
        const newPositions = Object.fromEntries(
          Object.keys(clonedPositions).flatMap((key) => {
            const KeyNumber = parseInt(key);
            if (KeyNumber < entityIndex) {
              return [[KeyNumber, clonedPositions[KeyNumber]]];
            } else if (KeyNumber === entityIndex) {
              return [];
            } else {
              return [[KeyNumber - 1, clonedPositions[KeyNumber]]];
            }
          })
        );

        clonedEntities.splice(entityIndex, 1);

        clonedEntities.map((entity) => {
          entity.relationsToEntityIndex = entity.relationsToEntityIndex?.flatMap(
            (index) => {
              return index < entityIndex
                ? [index]
                : index === entityIndex
                ? []
                : [index - 1];
            }
          );
          return entity;
        });

        setEntitiesPosition(newPositions);
        setFieldValue(`entities`, [...clonedEntities]);
      } else {
        setError(
          new Error(
            "To delete an entity, first remove all its fields and relations"
          )
        );
      }
    },
    [values, entitiesPosition, setFieldValue, setEntitiesPosition]
  );

  const handleAddEntity = useCallback(
    (entityIndex: number) => {
      const entities = cloneDeep(values.entities);
      const currentLength = entities.length;
      const relations = entities[entityIndex].relationsToEntityIndex || [];
      const currentEntity = entities[entityIndex];

      currentEntity.relationsToEntityIndex = [...relations, currentLength];

      const clonedPositions = cloneDeep(entitiesPosition);
      const sourcePosition = clonedPositions[entityIndex];

      const newLeft =
        sourcePosition.left +
        sourcePosition.width +
        DEFAULT_ENTITY_HORIZONTAL_GAP;
      const newRight = newLeft + DEFAULT_ENTITY_WIDTH;
      const minMaxValues: number[] = [];

      Object.entries(clonedPositions).forEach(([key, value]) => {
        if (!(value.left > newRight || value.left + value.width < newLeft)) {
          minMaxValues.push(value.top);
          minMaxValues.push(value.top + value.height);
        }
      });

      const min = Math.min(...minMaxValues);
      const max = Math.max(...minMaxValues);

      let newTop = 0;

      if (min >= DEFAULT_ENTITY_HEIGHT + DEFAULT_ENTITY_VERTICAL_GAP) {
        newTop = 0;
      } else {
        newTop = max + DEFAULT_ENTITY_VERTICAL_GAP;
      }

      const newPosition = {
        top: newTop,
        left: newLeft,
        width: DEFAULT_ENTITY_WIDTH,
        height: DEFAULT_ENTITY_HEIGHT,
      };
      clonedPositions[currentLength] = newPosition;

      setEntitiesPosition(clonedPositions);

      setFieldValue(`entities`, [
        ...entities,
        {
          name: "",
          fields: [],
        },
      ]);
      resetEditableElements();
      setEditedEntity(currentLength);
    },
    [setFieldValue, values.entities, resetEditableElements, entitiesPosition]
  );

  return (
    <div className={CLASS_NAME}>
      <div className={`${CLASS_NAME}__toolbar`}>
        <Button
          className={`${CLASS_NAME}__toolbar__button`}
          buttonStyle={EnumButtonStyle.Clear}
          type="button"
          onClick={zoomIn}
          icon="zoom_in"
        />
        <Button
          className={`${CLASS_NAME}__toolbar__button`}
          buttonStyle={EnumButtonStyle.Clear}
          type="button"
          onClick={zoomOut}
          icon="zoom_out"
        />

        <Button
          className={`${CLASS_NAME}__toolbar__button`}
          buttonStyle={EnumButtonStyle.Clear}
          type="button"
          onClick={zoomReset}
          icon="maximize_2"
        />
      </div>
      <div className={`${CLASS_NAME}__scroll`}>
        <div className={`${CLASS_NAME}__scroll-content`}>
          <EntitiesDiagramRelations entities={values.entities} />

          <div
            className={`${CLASS_NAME}__scale`}
            style={{ transform: `scale(${zoomLevel})` }}
          >
            <DragDropContext
              onDragEnd={onFieldDragEnd}
              onDragStart={onFieldDragStart}
            >
              {values.entities.map((entity, index) => (
                <EntitiesDiagramEntity
                  zoomLevel={zoomLevel}
                  key={`entity_${index}`}
                  entity={entity}
                  entityIndex={index}
                  positionData={entitiesPosition[index]}
                  onDrag={handleEntityDrag}
                  onEditField={handleEditField}
                  onEditEntity={handleEditEntity}
                  onAddEntity={handleAddEntity}
                  onDeleteEntity={handleDeleteEntity}
                  editedFieldIdentifier={editedField}
                  editedEntity={editedEntity}
                />
              ))}
            </DragDropContext>
          </div>
        </div>
      </div>
      <Snackbar
        open={Boolean(error)}
        message={error?.message}
        onClose={clearError}
      />
    </div>
  );
}
Example #22
Source File: AppTable.tsx    From flame with MIT License 4 votes vote down vote up
AppTable = (props: Props): JSX.Element => {
  const {
    apps: { apps },
    config: { config },
  } = useSelector((state: State) => state);

  const dispatch = useDispatch();
  const { pinApp, deleteApp, reorderApps, createNotification, updateApp } =
    bindActionCreators(actionCreators, dispatch);

  const [localApps, setLocalApps] = useState<App[]>([]);

  // Copy apps array
  useEffect(() => {
    setLocalApps([...apps]);
  }, [apps]);

  const dragEndHanlder = (result: DropResult): void => {
    if (config.useOrdering !== 'orderId') {
      createNotification({
        title: 'Error',
        message: 'Custom order is disabled',
      });
      return;
    }

    if (!result.destination) {
      return;
    }

    const tmpApps = [...localApps];
    const [movedApp] = tmpApps.splice(result.source.index, 1);
    tmpApps.splice(result.destination.index, 0, movedApp);

    setLocalApps(tmpApps);
    reorderApps(tmpApps);
  };

  // Action handlers
  const deleteAppHandler = (id: number, name: string) => {
    const proceed = window.confirm(`Are you sure you want to delete ${name}?`);

    if (proceed) {
      deleteApp(id);
    }
  };

  const updateAppHandler = (id: number) => {
    const app = apps.find((a) => a.id === id) as App;
    props.openFormForUpdating(app);
  };

  const pinAppHandler = (id: number) => {
    const app = apps.find((a) => a.id === id) as App;
    pinApp(app);
  };

  const changeAppVisibiltyHandler = (id: number) => {
    const app = apps.find((a) => a.id === id) as App;
    updateApp(id, { ...app, isPublic: !app.isPublic });
  };

  return (
    <Fragment>
      <Message isPrimary={false}>
        {config.useOrdering === 'orderId' ? (
          <p>You can drag and drop single rows to reorder application</p>
        ) : (
          <p>
            Custom order is disabled. You can change it in the{' '}
            <Link to="/settings/general">settings</Link>
          </p>
        )}
      </Message>

      <DragDropContext onDragEnd={dragEndHanlder}>
        <Droppable droppableId="apps">
          {(provided) => (
            <Table
              headers={['Name', 'URL', 'Icon', 'Visibility', 'Actions']}
              innerRef={provided.innerRef}
            >
              {localApps.map((app: App, index): JSX.Element => {
                return (
                  <Draggable
                    key={app.id}
                    draggableId={app.id.toString()}
                    index={index}
                  >
                    {(provided, snapshot) => {
                      const style = {
                        border: snapshot.isDragging
                          ? '1px solid var(--color-accent)'
                          : 'none',
                        borderRadius: '4px',
                        ...provided.draggableProps.style,
                      };

                      return (
                        <tr
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          ref={provided.innerRef}
                          style={style}
                        >
                          <td style={{ width: '200px' }}>{app.name}</td>
                          <td style={{ width: '200px' }}>{app.url}</td>
                          <td style={{ width: '200px' }}>{app.icon}</td>
                          <td style={{ width: '200px' }}>
                            {app.isPublic ? 'Visible' : 'Hidden'}
                          </td>

                          {!snapshot.isDragging && (
                            <TableActions
                              entity={app}
                              deleteHandler={deleteAppHandler}
                              updateHandler={updateAppHandler}
                              pinHanlder={pinAppHandler}
                              changeVisibilty={changeAppVisibiltyHandler}
                            />
                          )}
                        </tr>
                      );
                    }}
                  </Draggable>
                );
              })}
            </Table>
          )}
        </Droppable>
      </DragDropContext>
    </Fragment>
  );
}
Example #23
Source File: index.tsx    From scorpio-h5-design with MIT License 4 votes vote down vote up
export default function() {
  const [isOrdering, setIsOrdering] = useBoolean(false);
  const {
    isDraging, pageSchema, selectPageIndex, dragingComponentIndex, selectComponent,
    onDragEnter, onDragLeave, onDrop, onSelectComponent, onSortEnd, showSelectComponentBorder,
  } = useModel('bridge');
  let components:any[] = [];
  if (pageSchema.length > 0 && selectPageIndex !== -1) {
    components = pageSchema[selectPageIndex].components;
  }

  return (
    <DragDropContext
      onDragStart={()=>{
        setIsOrdering.setTrue();
      }}
      onDragEnd={(result: DropResult)=>{
        onSortEnd(result, components);
        setIsOrdering.setFalse();
      }}
    >
      <Droppable droppableId="droppable">
        {(provided, snapshot) => (
          <div
            {...provided.droppableProps}
            ref={provided.innerRef}
          >
            {components.length > 0 && components.map((item: any, index: any) => (
              <Draggable key={item.uuid} draggableId={item.uuid} index={index}>
                {(provided, snapshot) => (
                  <>
                    <div
                      className={classnames(
                        'h5-canvas-empty-block',
                        {
                          show: isDraging,
                          over: dragingComponentIndex === index,
                        }
                      )}
                      onDragEnter={() => { onDragEnter(index); }}
                      onDragLeave={() => { onDragLeave(); }}
                      onDragOver={(event) => { event.preventDefault(); }}
                      onDrop={(event)=>{onDrop(event, index);}}
                    />
                    <div
                      className={classnames(
                        'h5-canvas-block',
                        {
                          'blur': !snapshot.isDragging && isOrdering,
                          'isSelected': selectComponent?.uuid === item.uuid,
                          'border': selectComponent?.uuid === item.uuid && showSelectComponentBorder,
                        }
                      )}
                      ref={provided.innerRef}
                      {...provided.draggableProps}
                      {...provided.dragHandleProps}
                      onClick={() => { onSelectComponent(item.uuid); }}
                    >
                      <DynamicComponent
                        id={item._id}
                        uuid={item.uuid}
                        isSelected={selectComponent?.uuid === item.uuid}
                        componentProps={item.props}
                        containerProps={item.containerProps}
                      />
                    </div>
                  </>
                )}
              </Draggable>
            ))}
            {provided.placeholder}
            <div
              className={classnames(
                'h5-canvas-empty-block',
                {
                  show: isDraging,
                  over: dragingComponentIndex === components.length,
                }
              )}
              onDragEnter={() => { onDragEnter(components.length); }}
              onDragLeave={() => { onDragLeave(); }}
              onDragOver={(event) => { event.preventDefault(); }}
              onDrop={(event)=>{onDrop(event, components.length);}}
            />
          </div>
        )}
      </Droppable>
    </DragDropContext>
  );
}
Example #24
Source File: bridge.ts    From scorpio-h5-design with MIT License 4 votes vote down vote up
/**
 * 这份状态会在父页面和iframe中实时同步
 */
export default function bridge() {
  /** 是否拖拽中 */
  const [isDraging, setIsDraging] = useState(false);
  /** 当前拖拽的组件 */
  const [dragComponent, setDragComponent] = useState<IComponentSchema | undefined>(undefined);
  /** 页面id */
  const [pageId, setPageId] = useState<string>();
  /** 页面结构schema */
  const [pageSchema, setPageSchema] = useState<IPageSchema[]>([]);
  /** TODO: 使用索引不太好(若增加页面排序) */
  const [selectPageIndex, setSelectPageIndex] = useState(-1);
  /** 当前拖拽元素即将插入的位置索引(从0开始,-1为初始值) */
  const [dragingComponentIndex, setDragingComponentIndex] = useState(-1);
  /** 当前选中的组件 */
  const [selectComponentId, setSelectComponentId] = useState<string | undefined>(undefined);
  /** 是否显示选中组件边界 */
  const [showSelectComponentBorder, setShowSelectComponentBorder] = useState(() => window.localStorage.getItem('selectArea_borderVisible') !== 'false');
  const selectPage = pageSchema[selectPageIndex];
  const selectComponent = (selectPage && selectComponentId) ? pageSchema[selectPageIndex].components.find((item) => item.uuid === selectComponentId) : undefined;

  /**
   * 根据对象更新state
   * @param state
   * @param isSyncState 是否在iframe之间同步状态,默认true
   */
  const setStateByObjectKeys = function(state: {
    isDraging?: boolean;
    dragComponent?: IComponentSchema;
    pageId?: string;
    pageSchema?: IPageSchema[];
    selectPageIndex?: number;
    selectComponentId?: string;
    dragingComponentIndex?: number;
    showSelectComponentBorder?: boolean;
  }, isSyncState = true) {
    // 遍历keyset,可以避免不必要的渲染
    Object.keys(state).forEach((key) => {
      if (key === 'isDraging') {
        // @ts-expect-error
        setIsDraging(state.isDraging);
      }
      if (key === 'dragComponent') {
        setDragComponent(state.dragComponent);
      }
      if (key === 'pageId') {
        setPageId(state.pageId);
      }
      if (key === 'pageSchema') {
        // @ts-expect-error
        setPageSchema(state.pageSchema);
      }
      if (key === 'selectPageIndex') {
        // @ts-expect-error
        setSelectPageIndex(state.selectPageIndex);
      }
      if (key === 'selectComponentId') {
        setSelectComponentId(state.selectComponentId);
      }
      if (key === 'dragingComponentIndex') {
        // @ts-expect-error
        setDragingComponentIndex(state.dragingComponentIndex);
      }
      if (key === 'showSelectComponentBorder') {
        // @ts-expect-error
        setShowSelectComponentBorder(state.showSelectComponentBorder);
      }
    });
    // iframe之间同步状态
    if (isSyncState) {
      syncState({
        payload: state,
        type: IMessageType.syncState,
      });
    }
    // TODO:同步移动端页面快照(性能问题暂不使用)
    if (!isMobile() && !isSyncState && window.postmate_mobile) {
      // capture.run();
    }
  };

  // 组件拖拽开始
  const onDragStart = function(component: IComponentSchema) {
    // 组件增加唯一标识
    component.uuid = uuidv4();
    setStateByObjectKeys({
      isDraging: true,
      dragComponent: component,
    });
  };

  // 组件拖拽结束
  const onDragEnd = function() {
    setStateByObjectKeys({
      isDraging: false,
    });
  };

  /** 拖拽-进入 */
  const onDragEnter = function(index: number) {
    setStateByObjectKeys({
      dragingComponentIndex: index,
    });
  };

  /** 拖拽-离开 */
  const onDragLeave = function() {
    setStateByObjectKeys({
      dragingComponentIndex: -1,
    });
  };

  /** 拖拽-放置 */
  const onDrop = function(ev: React.DragEvent<HTMLDivElement>, index: number) {
    ev.preventDefault();
    const components = pageSchema[selectPageIndex].components;
    components.splice(index, 0, dragComponent as IComponentSchema);
    setStateByObjectKeys({
      dragingComponentIndex: -1,
      dragComponent: undefined,
      pageSchema,
    });
  };

  /** 重排 */
  const reorder = (components: IComponentSchema[], startIndex: number, endIndex: number) => {
    const result = Array.from(components);
    const [removed] = result.splice(startIndex, 1);
    result.splice(endIndex, 0, removed);
    return result;
  };

  /**
   * 排序拖拽-放置
   * @TODO 视图会闪烁
  */
  const onSortEnd = function(result: DropResult, currentPageComponents: IComponentSchema[]) {
    if (!result.destination) {
      return;
    }
    const reorderedComponents = reorder(currentPageComponents, result.source.index, result.destination.index);
    pageSchema[selectPageIndex].components = reorderedComponents;
    setStateByObjectKeys({
      pageSchema,
    });
  };

  /** 选中组件 */
  const onSelectComponent = function(id: string) {
    if (selectComponentId !== id) {
      setStateByObjectKeys({
        selectComponentId: id,
      });
    }
  };

  const changeContainerPropsState = function(key: string, value: unknown) {
    // @ts-expect-error
    selectComponent.containerProps[key] = value;
    setStateByObjectKeys({
      pageSchema: [...pageSchema],
    });
  };

  return {
    isDraging,
    dragComponent,
    pageId,
    pageSchema,
    selectPageIndex,
    dragingComponentIndex,
    setStateByObjectKeys,
    onDragStart,
    onDragEnd,
    onDragEnter,
    onDragLeave,
    onDrop,
    onSelectComponent,
    onSortEnd,
    selectComponent,
    changeContainerPropsState,
    selectComponentId,
    showSelectComponentBorder,
    selectPage,
  };
}
Example #25
Source File: DownloadQueue.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function DownloadQueue() {
    const [, setWsClient] = useState<WebSocket>();
    const [queueState, setQueueState] = useState<IQueue>(initialQueue);
    const { queue, status } = queueState;

    const history = useHistory();

    const theme = useTheme();

    const { setTitle, setAction } = useContext(NavbarContext);

    const toggleQueueStatus = () => {
        if (status === 'Stopped') {
            client.get('/api/v1/downloads/start');
        } else {
            client.get('/api/v1/downloads/stop');
        }
    };

    useEffect(() => {
        setTitle('Download Queue');

        setAction(() => {
            if (status === 'Stopped') {
                return (
                    <IconButton onClick={toggleQueueStatus} size="large">
                        <PlayArrowIcon />
                    </IconButton>
                );
            }
            return (
                <IconButton onClick={toggleQueueStatus} size="large">
                    <PauseIcon />
                </IconButton>
            );
        });
    }, [status]);

    useEffect(() => {
        const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
        wsc.onmessage = (e) => {
            setQueueState(JSON.parse(e.data));
        };

        setWsClient(wsc);
    }, []);

    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const onDragEnd = (result: DropResult) => {
    };

    if (queue.length === 0) {
        return <EmptyView message="No downloads" />;
    }

    return (
        <>
            <DragDropContext onDragEnd={onDragEnd}>
                <Droppable droppableId="droppable">
                    {(provided) => (
                        <List ref={provided.innerRef}>
                            {queue.map((item, index) => (
                                <Draggable
                                    key={`${item.mangaId}-${item.chapterIndex}`}
                                    draggableId={`${item.mangaId}-${item.chapterIndex}`}
                                    index={index}
                                >
                                    {(provided, snapshot) => (
                                        <ListItem
                                            ContainerProps={{ ref: provided.innerRef } as any}
                                            sx={{
                                                display: 'flex',
                                                justifyContent: 'flex-start',
                                                alignItems: 'flex-start',
                                                padding: 2,
                                                margin: '10px',
                                                '&:hover': {
                                                    backgroundColor: 'action.hover',
                                                    transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                                                },
                                                '&:active': {
                                                    backgroundColor: 'action.selected',
                                                    transition: 'background-color 100ms cubic-bezier(0.4, 0, 0.2, 1) 0ms',
                                                },
                                            }}
                                            onClick={() => history.push(`/manga/${item.chapter.mangaId}`)}
                                            {...provided.draggableProps}
                                            {...provided.dragHandleProps}
                                            style={getItemStyle(
                                                snapshot.isDragging,
                                                provided.draggableProps.style,
                                                theme.palette,
                                            )}
                                            ref={provided.innerRef}
                                        >
                                            <ListItemIcon sx={{ margin: 'auto 0' }}>
                                                <DragHandleIcon />
                                            </ListItemIcon>
                                            <Box sx={{ display: 'flex' }}>
                                                <Box sx={{ display: 'flex', flexDirection: 'column' }}>
                                                    <Typography variant="h5" component="h2">
                                                        {item.manga.title}
                                                    </Typography>
                                                    <Typography variant="caption" display="block" gutterBottom>
                                                        {`${item.chapter.name} `
                                                        + `(${(item.progress * 100).toFixed(2)}%)`
                                                        + ` => state: ${item.state}`}
                                                    </Typography>
                                                </Box>
                                            </Box>
                                            <IconButton
                                                sx={{ marginLeft: 'auto' }}
                                                onClick={(e) => {
                                                    // deleteCategory(index);
                                                    // prevent parent tags from getting the event
                                                    e.stopPropagation();
                                                }}
                                                size="large"
                                            >
                                                <DeleteIcon />
                                            </IconButton>
                                        </ListItem>
                                    )}
                                </Draggable>

                            ))}
                            {provided.placeholder}
                        </List>
                    )}
                </Droppable>
            </DragDropContext>
        </>
    );
}
Example #26
Source File: index.tsx    From peterportal-client with MIT License 4 votes vote down vote up
RoadmapPage: FC = () => {
  const dispatch = useAppDispatch();
  const showSearch = useAppSelector(state => state.roadmap.showSearch);

  const onDragEnd = useCallback((result: DropResult) => {
    if (result.reason === 'DROP') {
      // no destination or dragging to search bar
      if (!result.destination || result.destination.droppableId === 'search') {
        // removing from quarter
        if (result.source.droppableId != 'search') {
          let [yearIndex, quarterIndex] = result.source.droppableId.split('-');
          dispatch(deleteCourse({
            yearIndex: parseInt(yearIndex),
            quarterIndex: parseInt(quarterIndex),
            courseIndex: result.source.index
          }))
        }
        return;
      }

      let movePayload = {
        from: {
          yearIndex: -1,
          quarterIndex: -1,
          courseIndex: -1,
        },
        to: {
          yearIndex: -1,
          quarterIndex: -1,
          courseIndex: -1
        }
      };

      console.log(result.source.droppableId, '=>', result.destination.droppableId)

      // roadmap to roadmap has source
      if (result.source.droppableId != 'search') {
        let [yearIndex, quarterIndex] = result.source.droppableId.split('-');
        movePayload.from.yearIndex = parseInt(yearIndex);
        movePayload.from.quarterIndex = parseInt(quarterIndex);
        movePayload.from.courseIndex = result.source.index;
      }
      // search to roadmap has no source (use activeCourse in global state)      

      // both have destination
      let [yearIndex, quarterIndex] = result.destination.droppableId.split('-');
      movePayload.to.yearIndex = parseInt(yearIndex);
      movePayload.to.quarterIndex = parseInt(quarterIndex);
      movePayload.to.courseIndex = result.destination.index;

      console.log(movePayload);
      dispatch(moveCourse(movePayload));
    }
  }, []);

  const onDragUpdate = useCallback((initial: DragUpdate) => {
    console.log(initial)
  }, []);

  // do not conditionally renderer because it would remount planner which would discard unsaved changes
  const mobileVersion = <>
    <div className={`main-wrapper mobile ${showSearch ? 'hide' : ''}`}>
      <Planner />
    </div>
    <div className={`sidebar-wrapper mobile ${!showSearch ? 'hide' : ''}`}>
      <SearchSidebar />
    </div>
  </>

  const desktopVersion = <>
    <div className='main-wrapper'>
      <Planner />
    </div>
    <div className='sidebar-wrapper'>
      <SearchSidebar />
    </div>
  </>

  return (
    <>
      <div className='roadmap-page'>
        <AddCoursePopup />
        <DragDropContext onDragEnd={onDragEnd} onDragUpdate={onDragUpdate}>
          {isMobile && mobileVersion}
          {!isMobile && desktopVersion}
        </DragDropContext>
      </div>
    </>
  );
}
Example #27
Source File: PipelineEditComponents.tsx    From baleen3 with Apache License 2.0 4 votes vote down vote up
PipelineEditComponents: React.FC<PipelineEditComponentsProps> = ({
  id,
  type,
  components,
  addComponents,
  moveComponent,
  removeComponent,
  setComponentName,
  setComponentSettings,
}: PipelineEditComponentsProps) => {
  const handleDrag = (result: DropResult): void => {
    const { source, destination } = result

    if (destination === undefined) {
      return
    }

    if (destination.index === source.index) {
      return
    }

    moveComponent(source.index, destination.index)
  }
  return (
    <DragDropContext onDragEnd={handleDrag}>
      <Droppable droppableId={id}>
        {(provided: DroppableProvided): React.ReactElement => (
          <Column
            alignItems="center"
            {...provided.droppableProps}
            // eslint-disable-next-line @typescript-eslint/unbound-method
            ref={provided.innerRef}
          >
            {components.map((component, index) => {
              return (
                <Draggable
                  key={component.id}
                  draggableId={component.id}
                  index={index}
                >
                  {(
                    provider: DraggableProvided,
                    snapshot: DraggableStateSnapshot
                  ): React.ReactElement => (
                    <Column
                      width={1}
                      alignItems="center"
                      // eslint-disable-next-line @typescript-eslint/unbound-method
                      ref={provider.innerRef}
                      {...provider.draggableProps}
                    >
                      <PipelineEditComponentSeparator
                        onInsert={(components: ComponentInfo[]): void =>
                          addComponents(components, index)
                        }
                        isDragging={snapshot.isDragging}
                        type={type}
                      />
                      <PipelineComponent
                        {...provider.dragHandleProps}
                        type={type}
                        descriptor={component}
                        setName={setComponentName}
                        setSettings={setComponentSettings}
                        onDelete={removeComponent}
                      />
                    </Column>
                  )}
                </Draggable>
              )
            })}
            {provided.placeholder}
            <PipelineEditComponentSeparator
              onInsert={(components: ComponentInfo[]): void =>
                addComponents(components)
              }
              isDragging={false}
              type={type}
            />
          </Column>
        )}
      </Droppable>
    </DragDropContext>
  )
}
Example #28
Source File: main.tsx    From webminidisc with GNU General Public License v2.0 4 votes vote down vote up
Main = (props: {}) => {
    let dispatch = useDispatch();
    const disc = useShallowEqualSelector(state => state.main.disc);
    const deviceName = useShallowEqualSelector(state => state.main.deviceName);
    const deviceStatus = useShallowEqualSelector(state => state.main.deviceStatus);
    const { vintageMode } = useShallowEqualSelector(state => state.appState);

    const [selected, setSelected] = React.useState<number[]>([]);
    const [uploadedFiles, setUploadedFiles] = React.useState<File[]>([]);
    const [lastClicked, setLastClicked] = useState(-1);
    const [moveMenuAnchorEl, setMoveMenuAnchorEl] = React.useState<null | HTMLElement>(null);

    const handleShowMoveMenu = useCallback(
        (event: React.MouseEvent<HTMLButtonElement>) => {
            setMoveMenuAnchorEl(event.currentTarget);
        },
        [setMoveMenuAnchorEl]
    );
    const handleCloseMoveMenu = useCallback(() => {
        setMoveMenuAnchorEl(null);
    }, [setMoveMenuAnchorEl]);

    const handleMoveSelectedTrack = useCallback(
        (destIndex: number) => {
            dispatch(moveTrack(selected[0], destIndex));
            handleCloseMoveMenu();
        },
        [dispatch, selected, handleCloseMoveMenu]
    );

    const handleDrop = useCallback(
        (result: DropResult, provided: ResponderProvided) => {
            if (!result.destination) return;
            let sourceList = parseInt(result.source.droppableId),
                sourceIndex = result.source.index,
                targetList = parseInt(result.destination.droppableId),
                targetIndex = result.destination.index;
            dispatch(dragDropTrack(sourceList, sourceIndex, targetList, targetIndex));
        },
        [dispatch]
    );

    const handleShowDumpDialog = useCallback(() => {
        dispatch(dumpDialogActions.setVisible(true));
    }, [dispatch]);

    useEffect(() => {
        dispatch(listContent());
    }, [dispatch]);

    useEffect(() => {
        setSelected([]); // Reset selection if disc changes
    }, [disc]);

    const onDrop = useCallback(
        (acceptedFiles: File[], rejectedFiles: File[]) => {
            setUploadedFiles(acceptedFiles);
            dispatch(convertDialogActions.setVisible(true));
        },
        [dispatch]
    );

    const { getRootProps, getInputProps, isDragActive, open } = useDropzone({
        onDrop,
        accept: [`audio/*`, `video/mp4`],
        noClick: true,
    });

    const classes = useStyles();
    const tracks = useMemo(() => getSortedTracks(disc), [disc]);
    const groupedTracks = useMemo(() => getGroupedTracks(disc), [disc]);

    // Action Handlers
    const handleSelectTrackClick = useCallback(
        (event: React.MouseEvent, item: number) => {
            if (event.shiftKey && selected.length && lastClicked !== -1) {
                let rangeBegin = Math.min(lastClicked + 1, item),
                    rangeEnd = Math.max(lastClicked - 1, item);
                let copy = [...selected];
                for (let i = rangeBegin; i <= rangeEnd; i++) {
                    let index = copy.indexOf(i);
                    if (index === -1) copy.push(i);
                    else copy.splice(index, 1);
                }
                if (!copy.includes(item)) copy.push(item);
                setSelected(copy);
            } else if (selected.includes(item)) {
                setSelected(selected.filter(i => i !== item));
            } else {
                setSelected([...selected, item]);
            }
            setLastClicked(item);
        },
        [selected, setSelected, lastClicked, setLastClicked]
    );

    const handleSelectAllClick = useCallback(
        (event: React.ChangeEvent<HTMLInputElement>) => {
            if (selected.length < tracks.length) {
                setSelected(tracks.map(t => t.index));
            } else {
                setSelected([]);
            }
        },
        [selected, tracks]
    );

    const handleRenameTrack = useCallback(
        (event: React.MouseEvent, index: number) => {
            let track = tracks.find(t => t.index === index);
            if (!track) {
                return;
            }

            dispatch(
                batchActions([
                    renameDialogActions.setVisible(true),
                    renameDialogActions.setGroupIndex(null),
                    renameDialogActions.setCurrentName(track.title),
                    renameDialogActions.setCurrentFullWidthName(track.fullWidthTitle),
                    renameDialogActions.setIndex(track.index),
                ])
            );
        },
        [dispatch, tracks]
    );

    const handleRenameGroup = useCallback(
        (event: React.MouseEvent, index: number) => {
            let group = groupedTracks.find(g => g.index === index);
            if (!group) {
                return;
            }

            dispatch(
                batchActions([
                    renameDialogActions.setVisible(true),
                    renameDialogActions.setGroupIndex(index),
                    renameDialogActions.setCurrentName(group.title ?? ''),
                    renameDialogActions.setCurrentFullWidthName(group.fullWidthTitle ?? ''),
                    renameDialogActions.setIndex(-1),
                ])
            );
        },
        [dispatch, groupedTracks]
    );

    const handleRenameActionClick = useCallback(
        (event: React.MouseEvent) => {
            if (event.detail !== 1) return; //Event retriggering when hitting enter in the dialog
            handleRenameTrack(event, selected[0]);
        },
        [handleRenameTrack, selected]
    );

    const handleDeleteSelected = useCallback(
        (event: React.MouseEvent) => {
            dispatch(deleteTracks(selected));
        },
        [dispatch, selected]
    );

    const handleGroupTracks = useCallback(
        (event: React.MouseEvent) => {
            dispatch(groupTracks(selected));
        },
        [dispatch, selected]
    );

    const handleDeleteGroup = useCallback(
        (event: React.MouseEvent, index: number) => {
            dispatch(deleteGroup(index));
        },
        [dispatch]
    );

    const handleTogglePlayPauseTrack = useCallback(
        (event: React.MouseEvent, track: number) => {
            if (!deviceStatus) {
                return;
            }
            if (deviceStatus.track !== track) {
                dispatch(control('goto', track));
                dispatch(control('play'));
            } else if (deviceStatus.state === 'playing') {
                dispatch(control('pause'));
            } else {
                dispatch(control('play'));
            }
        },
        [dispatch, deviceStatus]
    );

    const canGroup = useMemo(() => {
        return (
            tracks.filter(n => n.group === null && selected.includes(n.index)).length === selected.length &&
            isSequential(selected.sort((a, b) => a - b))
        );
    }, [tracks, selected]);
    const selectedCount = selected.length;

    if (vintageMode) {
        const p = {
            disc,
            deviceName,

            selected,
            setSelected,
            selectedCount,

            tracks,
            uploadedFiles,
            setUploadedFiles,

            onDrop,
            getRootProps,
            getInputProps,
            isDragActive,
            open,

            moveMenuAnchorEl,
            setMoveMenuAnchorEl,

            handleShowMoveMenu,
            handleCloseMoveMenu,
            handleMoveSelectedTrack,
            handleShowDumpDialog,
            handleDeleteSelected,
            handleRenameActionClick,
            handleRenameTrack,
            handleSelectAllClick,
            handleSelectTrackClick,
        };
        return <W95Main {...p} />;
    }

    return (
        <React.Fragment>
            <Box className={classes.headBox}>
                <Typography component="h1" variant="h4">
                    {deviceName || `Loading...`}
                </Typography>
                <TopMenu />
            </Box>
            <Typography component="h2" variant="body2">
                {disc !== null ? (
                    <React.Fragment>
                        <span>{`${formatTimeFromFrames(disc.left, false)} left of ${formatTimeFromFrames(disc.total, false)} `}</span>
                        <Tooltip
                            title={
                                <React.Fragment>
                                    <span>{`${formatTimeFromFrames(disc.left * 2, false)} left in LP2 Mode`}</span>
                                    <br />
                                    <span>{`${formatTimeFromFrames(disc.left * 4, false)} left in LP4 Mode`}</span>
                                </React.Fragment>
                            }
                            arrow
                        >
                            <span className={classes.remainingTimeTooltip}>SP Mode</span>
                        </Tooltip>
                    </React.Fragment>
                ) : (
                    `Loading...`
                )}
            </Typography>
            <Toolbar
                className={clsx(classes.toolbar, {
                    [classes.toolbarHighlight]: selectedCount > 0,
                })}
            >
                {selectedCount > 0 ? (
                    <Checkbox
                        indeterminate={selectedCount > 0 && selectedCount < tracks.length}
                        checked={selectedCount > 0}
                        onChange={handleSelectAllClick}
                        inputProps={{ 'aria-label': 'select all tracks' }}
                    />
                ) : null}
                {selectedCount > 0 ? (
                    <Typography className={classes.toolbarLabel} color="inherit" variant="subtitle1">
                        {selectedCount} selected
                    </Typography>
                ) : (
                    <Typography component="h3" variant="h6" className={classes.toolbarLabel}>
                        {disc?.fullWidthTitle && `${disc.fullWidthTitle} / `}
                        {disc?.title || `Untitled Disc`}
                    </Typography>
                )}
                {selectedCount > 0 ? (
                    <React.Fragment>
                        <Tooltip title="Record from MD">
                            <Button aria-label="Record" onClick={handleShowDumpDialog}>
                                Record
                            </Button>
                        </Tooltip>
                    </React.Fragment>
                ) : null}

                {selectedCount > 0 ? (
                    <Tooltip title="Delete">
                        <IconButton aria-label="delete" onClick={handleDeleteSelected}>
                            <DeleteIcon />
                        </IconButton>
                    </Tooltip>
                ) : null}

                {selectedCount > 0 ? (
                    <Tooltip title={canGroup ? 'Group' : ''}>
                        <IconButton aria-label="group" disabled={!canGroup} onClick={handleGroupTracks}>
                            <CreateNewFolderIcon />
                        </IconButton>
                    </Tooltip>
                ) : null}

                {selectedCount > 0 ? (
                    <Tooltip title="Rename">
                        <IconButton aria-label="rename" disabled={selectedCount !== 1} onClick={handleRenameActionClick}>
                            <EditIcon />
                        </IconButton>
                    </Tooltip>
                ) : null}
            </Toolbar>
            <Box className={classes.main} {...getRootProps()} id="main">
                <input {...getInputProps()} />
                <Table size="small">
                    <TableHead>
                        <TableRow>
                            <TableCell className={classes.dragHandleEmpty}></TableCell>
                            <TableCell className={classes.indexCell}>#</TableCell>
                            <TableCell>Title</TableCell>
                            <TableCell align="right">Duration</TableCell>
                        </TableRow>
                    </TableHead>
                    <DragDropContext onDragEnd={handleDrop}>
                        <TableBody>
                            {groupedTracks.map((group, index) => (
                                <TableRow key={`${index}`}>
                                    <TableCell colSpan={4} style={{ padding: '0' }}>
                                        <Table size="small">
                                            <Droppable droppableId={`${index}`} key={`${index}`}>
                                                {(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
                                                    <TableBody
                                                        {...provided.droppableProps}
                                                        ref={provided.innerRef}
                                                        className={clsx({ [classes.hoveringOverGroup]: snapshot.isDraggingOver })}
                                                    >
                                                        {group.title !== null && (
                                                            <GroupRow
                                                                group={group}
                                                                onRename={handleRenameGroup}
                                                                onDelete={handleDeleteGroup}
                                                            />
                                                        )}
                                                        {group.title === null && group.tracks.length === 0 && (
                                                            <TableRow style={{ height: '1px' }} />
                                                        )}
                                                        {group.tracks.map((t, tidx) => (
                                                            <Draggable
                                                                draggableId={`${group.index}-${t.index}`}
                                                                key={`t-${t.index}`}
                                                                index={tidx}
                                                            >
                                                                {(provided: DraggableProvided) => (
                                                                    <TrackRow
                                                                        track={t}
                                                                        draggableProvided={provided}
                                                                        inGroup={group.title !== null}
                                                                        isSelected={selected.includes(t.index)}
                                                                        trackStatus={getTrackStatus(t, deviceStatus)}
                                                                        onSelect={handleSelectTrackClick}
                                                                        onRename={handleRenameTrack}
                                                                        onTogglePlayPause={handleTogglePlayPauseTrack}
                                                                    />
                                                                )}
                                                            </Draggable>
                                                        ))}
                                                        {provided.placeholder}
                                                    </TableBody>
                                                )}
                                            </Droppable>
                                        </Table>
                                    </TableCell>
                                </TableRow>
                            ))}
                        </TableBody>
                    </DragDropContext>
                </Table>
                {isDragActive ? (
                    <Backdrop className={classes.backdrop} open={isDragActive}>
                        Drop your Music to Upload
                    </Backdrop>
                ) : null}
            </Box>
            <Fab color="primary" aria-label="add" className={classes.add} onClick={open}>
                <AddIcon />
            </Fab>

            <UploadDialog />
            <RenameDialog />
            <ErrorDialog />
            <ConvertDialog files={uploadedFiles} />
            <RecordDialog />
            <DumpDialog trackIndexes={selected} />
            <AboutDialog />
            <PanicDialog />
        </React.Fragment>
    );
}
Example #29
Source File: Categories.tsx    From Tachidesk-WebUI with Mozilla Public License 2.0 4 votes vote down vote up
export default function Categories() {
    const { setTitle, setAction } = useContext(NavbarContext);
    useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);

    const [categories, setCategories] = useState<ICategory[]>([]);
    const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
    const [dialogOpen, setDialogOpen] = useState<boolean>(false);
    const [dialogName, setDialogName] = useState<string>('');
    const [dialogDefault, setDialogDefault] = useState<boolean>(false);
    const theme = useTheme();

    const [updateTriggerHolder, setUpdateTriggerHolder] = useState<number>(0); // just a hack
    const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack

    useEffect(() => {
        if (!dialogOpen) {
            client.get('/api/v1/category/')
                .then((response) => response.data)
                .then((data) => { if (data.length > 0 && data[0].name === 'Default') data.shift(); return data; })
                .then((data) => setCategories(data));
        }
    }, [updateTriggerHolder]);

    const categoryReorder = (list: ICategory[], from: number, to: number) => {
        const formData = new FormData();
        formData.append('from', `${from + 1}`);
        formData.append('to', `${to + 1}`);
        client.patch('/api/v1/category/reorder', formData)
            .finally(() => triggerUpdate());

        // also move it in local state to avoid jarring moving behviour...
        const result = Array.from(list);
        const [removed] = result.splice(from, 1);
        result.splice(to, 0, removed);
        return result;
    };

    const onDragEnd = (result: DropResult) => {
        // dropped outside the list?
        if (!result.destination) {
            return;
        }

        setCategories(categoryReorder(
            categories,
            result.source.index,
            result.destination.index,
        ));
    };

    const resetDialog = () => {
        setDialogName('');
        setDialogDefault(false);
        setCategoryToEdit(-1);
    };

    const handleDialogOpen = () => {
        resetDialog();
        setDialogOpen(true);
    };

    const handleEditDialogOpen = (index:number) => {
        setDialogName(categories[index].name);
        setDialogDefault(categories[index].default);
        setCategoryToEdit(index);
        setDialogOpen(true);
    };

    const handleDialogCancel = () => {
        setDialogOpen(false);
    };

    const handleDialogSubmit = () => {
        setDialogOpen(false);

        const formData = new FormData();
        formData.append('name', dialogName);
        formData.append('default', dialogDefault.toString());

        if (categoryToEdit === -1) {
            client.post('/api/v1/category/', formData)
                .finally(() => triggerUpdate());
        } else {
            const category = categories[categoryToEdit];
            client.patch(`/api/v1/category/${category.id}`, formData)
                .finally(() => triggerUpdate());
        }
    };

    const deleteCategory = (index:number) => {
        const category = categories[index];
        client.delete(`/api/v1/category/${category.id}`)
            .finally(() => triggerUpdate());
    };

    return (
        <>
            <DragDropContext onDragEnd={onDragEnd}>
                <Droppable droppableId="droppable">
                    {(provided) => (
                        <List ref={provided.innerRef}>
                            {categories.map((item, index) => (
                                <Draggable
                                    key={item.id}
                                    draggableId={item.id.toString()}
                                    index={index}
                                >
                                    {(provided, snapshot) => (
                                        <ListItem
                                            ContainerProps={{ ref: provided.innerRef } as any}
                                            {...provided.draggableProps}
                                            {...provided.dragHandleProps}
                                            style={getItemStyle(
                                                snapshot.isDragging,
                                                provided.draggableProps.style,
                                                theme.palette,
                                            )}
                                            ref={provided.innerRef}
                                        >
                                            <ListItemIcon>
                                                <DragHandleIcon />
                                            </ListItemIcon>
                                            <ListItemText
                                                primary={item.name}
                                            />
                                            <IconButton
                                                onClick={() => {
                                                    handleEditDialogOpen(index);
                                                }}
                                                size="large"
                                            >
                                                <EditIcon />
                                            </IconButton>
                                            <IconButton
                                                onClick={() => {
                                                    deleteCategory(index);
                                                }}
                                                size="large"
                                            >
                                                <DeleteIcon />
                                            </IconButton>
                                        </ListItem>
                                    )}
                                </Draggable>
                            ))}
                            {provided.placeholder}
                        </List>
                    )}
                </Droppable>
            </DragDropContext>
            <Fab
                color="primary"
                aria-label="add"
                style={{
                    position: 'absolute',
                    bottom: theme.spacing(2),
                    right: theme.spacing(2),
                }}
                onClick={handleDialogOpen}
            >
                <AddIcon />
            </Fab>
            <Dialog open={dialogOpen} onClose={handleDialogCancel}>
                <DialogTitle id="form-dialog-title">
                    {categoryToEdit === -1 ? 'New Catalog' : 'Edit Catalog'}
                </DialogTitle>
                <DialogContent>
                    <TextField
                        autoFocus
                        margin="dense"
                        id="name"
                        label="Category Name"
                        type="text"
                        fullWidth
                        value={dialogName}
                        onChange={(e) => setDialogName(e.target.value)}
                    />
                    <FormControlLabel
                        control={(
                            <Checkbox
                                checked={dialogDefault}
                                onChange={(e) => setDialogDefault(e.target.checked)}
                                color="default"
                            />
                        )}
                        label="Default category when adding new manga to library"
                    />
                </DialogContent>
                <DialogActions>
                    <Button onClick={handleDialogCancel} color="primary">
                        Cancel
                    </Button>
                    <Button onClick={handleDialogSubmit} color="primary">
                        Submit
                    </Button>
                </DialogActions>
            </Dialog>
        </>
    );
}