Skip to content

Commit

Permalink
feat: implement of repo collection [OSPP 2023] (#713)
Browse files Browse the repository at this point in the history
* feat: developed RepoCollection Button through antd component

* feat: implement LocalStorage,SetDefault,DeleteCollection,AddCurRepo feature

* style: yarn run prettier for index.scss

* feat: refactor view

* feat: implement storage feature

* feat: implement collection modal from popup button

* feat: implement collection modal using tabs and repo list beside

* feat: implement collection modal from repository dropdown click

* feat: solve reopen modal problem

* feat: implement collection editor function (quick import need to be done)

* feat: add judgment for whether there is initial data

* feat: create CollectionEditor component

* feat: use GitHub REST API to get repo and its description

* feat: implement CollectionEditor add and edit feature

* feat: solve formatting and naming issues

* feat: refactor data structure

* refactor: implement the collection button in the original GitHub style (step 1)

* chore: update yarn.lock

* fix: update module import

* chore: update yarn.lock

* fix: duplicate rendering after tab switches

* fix: prevent duplicate finally

* refactor: give CollectionModal a separate directory

* refactor: CollectionList (30%) and AddToCollections (1%)

* feat: support basic interactions with CollectionButton

* chore: disable charts-design

* chore: allow overflow when there are many exsiting collections

* chore: add comments

* chore: add manage button in AddToCollections

* refactor: a possible final directory

* chore: support to open modal from CollectionList

* fix: fix typo

* feat: add confirm check before collection deletion

* feat: set default key for Table and add Radio for quick import (User/Organization)

* refactor: move custom type definition to context.ts

* refactor: use constate to better manage locally global state

* feat: store and useStore for repo-collection

* chore: update repo name after turbo:restore

* feat: replace some part with real data

* feat: AddToCollections(100%)

* fix: a simple lock mechanism to prevent concurrent updates in store.ts

* feat: implement CollectionDisplayModal and rename filename

* refactor: rename view.tsx

* feat: add divider for footer in AddToCollections

* refactor: remove redundant initialization

* refactor: rollback and delete displayModal

* feat: implement of CollectionManageModal

* feat: implement of CollectionEditor confirm

* feat: use await/async function

* chore: fix typo and add validator for duplicate collection name

* style: maximize the collection modal size

---------

Co-authored-by: Lam Tang <tangyenan@gmail.com>
  • Loading branch information
andyhuang18 and tyn1998 authored Oct 22, 2023
1 parent 86a3357 commit e83ec15
Show file tree
Hide file tree
Showing 14 changed files with 1,299 additions and 18 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "Hypertrons Chromium Extension",
"license": "Apache",
"engines": {
"node": ">=16.14"
"node": ">=18"
},
"scripts": {
"build": "cross-env NODE_ENV='production' BABEL_ENV='production' node utils/build.js",
Expand All @@ -28,6 +28,7 @@
"antd": "^5.9.1",
"buffer": "^6.0.3",
"colorthief": "^2.4.0",
"constate": "^3.3.2",
"delay": "^5.0.0",
"dom-loaded": "^3.0.0",
"echarts": "^5.3.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import { useRepoCollectionContext } from '../context';
import { Collection } from '../context/store';

import React, { useEffect, useState } from 'react';

const CheckListItem = (
collection: Collection,
onChange: (collectionId: Collection['id'], checked: boolean) => void,
checked: boolean
) => {
const handleChange = () => {
onChange(collection.id, checked);
};

return (
<div
key={collection.id}
className="form-checkbox mr-3 ml-6 ml-sm-5 mb-2 mt-0"
>
<label className="f5 text-normal">
<input
type="checkbox"
value={collection.id}
checked={checked}
onChange={handleChange}
/>
{collection.name}
</label>
</div>
);
};

/**
* The modal for quickly adding the current repository to existing collections (also for removing)
*/
export const AddToCollections = () => {
const {
currentRepositoryId,
currentRepositoryCollections,
allCollections,
updaters,
hideAddToCollections,
setHideAddToCollections,
setHideCollectionList,
setShowManageModal,
} = useRepoCollectionContext();

const [checkedCollectionIds, setCheckedCollectionIds] = useState<
Collection['id'][]
>([]);

const resetCheckboxes = () => {
setCheckedCollectionIds(currentRepositoryCollections.map((c) => c.id));
};

// reset checkboxes when currentRepositoryCollections changes
useEffect(() => {
resetCheckboxes();
}, [currentRepositoryCollections]);

const handleCheckChange = (
collectionId: Collection['id'],
checked: boolean
) => {
if (checked) {
setCheckedCollectionIds(
checkedCollectionIds.filter((id) => id !== collectionId)
);
} else {
setCheckedCollectionIds([...checkedCollectionIds, collectionId]);
}
};

const goToCollectionList = () => {
setHideAddToCollections(true);
setHideCollectionList(false);
};

const apply = () => {
// add/remove relations
const toAdd = checkedCollectionIds.filter(
(id) => !currentRepositoryCollections.some((c) => c.id === id)
);
const toRemove = currentRepositoryCollections.filter(
(c) => !checkedCollectionIds.includes(c.id)
);
toAdd &&
updaters.addRelations(
toAdd.map((id) => ({
collectionId: id,
repositoryId: currentRepositoryId,
}))
);
toRemove &&
updaters.removeRelations(
toRemove.map((c) => ({
collectionId: c.id,
repositoryId: currentRepositoryId,
}))
);

goToCollectionList();
};

const cancel = () => {
resetCheckboxes();

goToCollectionList();
};

const manage = () => {
// open modal to manage collections
setShowManageModal(true);
};

// if the ids of currentRepositoryCollections are the same as the ids of selectedCollectionIds, then the "Apply" button should be disabled
let isApplyDisabled: boolean;
if (currentRepositoryCollections.length !== checkedCollectionIds.length) {
isApplyDisabled = false;
} else {
isApplyDisabled = currentRepositoryCollections.every((c) =>
checkedCollectionIds.includes(c.id)
);
}

return (
<div
className="notifications-component-dialog"
hidden={hideAddToCollections}
>
<div className="SelectMenu-modal notifications-component-dialog-modal overflow-visible">
<header className="d-none d-sm-flex flex-items-start pt-1">
<button
className="border-0 px-2 pt-1 m-0 Link--secondary f5"
style={{ backgroundColor: 'transparent' }}
type="button"
onClick={goToCollectionList}
>
<svg
style={{ position: 'relative', left: '2px', top: '1px' }}
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
className="octicon octicon-arrow-left"
>
<path d="M7.78 12.53a.75.75 0 0 1-1.06 0L2.47 8.28a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L4.81 7h7.44a.75.75 0 0 1 0 1.5H4.81l2.97 2.97a.75.75 0 0 1 0 1.06Z"></path>
</svg>
</button>

<h1 className="pt-1 pr-4 pb-0 pl-0 f5 text-bold">
Add to collections
</h1>
</header>
<legend>
<div className="text-small color-fg-muted pt-0 pr-3 pb-3 pl-6 pl-sm-5 border-bottom mb-3">
Select collections you want the current repository to be added to.
</div>
</legend>
{/* Checklist */}
<div style={{ maxHeight: '200px', overflowY: 'auto' }}>
{allCollections.map((collection) => {
const checked = checkedCollectionIds.includes(collection.id);
return CheckListItem(collection, handleCheckChange, checked);
})}
</div>
{/* 3 buttons */}
<footer className="SelectMenu-footer">
<div className="pt-2 pb-3 px-3 d-flex flex-justify-start flex-row-reverse">
<button
disabled={isApplyDisabled}
className="btn-primary btn-sm btn ml-2"
onClick={apply}
>
Apply
</button>
<button className="btn-sm btn ml-2" onClick={cancel}>
Cancel
</button>
<button className="btn-sm btn" onClick={manage}>
Manage
</button>
</div>
</footer>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useRepoCollectionContext } from '../context';
import { Collection } from '../context/store';

import React from 'react';

const ListItem = (
collection: Collection,
onClick: (collectionId: Collection['id']) => void
) => {
const handleClick = () => {
onClick(collection.id);
};

return (
<div
key={collection.id}
className="SelectMenu-item flex-items-start btn border-0 rounded-0"
onClick={handleClick}
>
<span className="text-small text-normal wb-break-all">
{collection.name}
</span>
</div>
);
};

/**
* The modal that shows the collections that the repo belongs to
*/
export const CollectionList = () => {
const {
currentRepositoryCollections,
hideCollectionList,
setHideAddToCollections,
setHideCollectionList,
setSelectedCollection,
setShowManageModal,
} = useRepoCollectionContext();

const handleCollectionClick = (collectionId: Collection['id']) => {
setSelectedCollection(collectionId);
setShowManageModal(true);
};

const goToAddToCollections = () => {
setHideAddToCollections(false);
setHideCollectionList(true);
};

return (
<div className="SelectMenu" hidden={hideCollectionList}>
<div className="SelectMenu-modal">
<button
className="SelectMenu-closeButton position-absolute right-0 m-2"
type="button"
data-toggle-for="collection-button-details"
>
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
className="octicon octicon-x"
>
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
</svg>
</button>
<div className="d-flex flex-column flex-1 overflow-hidden">
<div className="SelectMenu-list">
{/* header */}
<header className="SelectMenu-header">
<h3 className="SelectMenu-title">
Contained in these collections
</h3>
<button
className="SelectMenu-closeButton"
type="button"
aria-label="Close menu"
data-toggle-for="collection-button-details"
>
<svg
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
width="16"
data-view-component="true"
className="octicon octicon-x"
>
<path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z"></path>
</svg>
</button>
</header>
{/* list */}
<div className="overflow-y-auto" style={{ maxHeight: '340px' }}>
{currentRepositoryCollections.map((collection) =>
ListItem(collection, handleCollectionClick)
)}
</div>
{/* footer */}
<footer className="SelectMenu-footer p-0 position-sticky">
<div
className="SelectMenu-item btn rounded-0 border-bottom-0 text-normal f6"
onClick={goToAddToCollections}
>
<svg
width="20"
aria-hidden="true"
height="16"
viewBox="0 0 16 16"
version="1.1"
data-view-component="true"
className="octicon octicon-plus mr-2 text-center"
>
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path>
</svg>
Add to collections
</div>
</footer>
</div>
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { CollectionList } from './CollectionList';
import { AddToCollections } from './AddToCollections';
import { useRepoCollectionContext } from '../context';

import React from 'react';
import { FundProjectionScreenOutlined } from '@ant-design/icons';

/**
* The "Collections" button, which is in the left of the "Edit Pins" button
*/
export const CollectionButton = () => {
const { currentRepositoryCollections } = useRepoCollectionContext();

return (
<div className="f5 position-relative">
<details
id="collection-button-details"
className="details-reset details-overlay f5 position-relative"
>
<summary className="btn-sm btn" role="button">
<span>
<FundProjectionScreenOutlined
style={{ fontSize: '14px', color: 'var(--color-fg-muted)' }}
/>
{' Collections '}
</span>
<span
data-pjax-replace="true" // not confirmed to be necessary
data-turbo-replace="true" // this one, either.
className="Counter"
>
{currentRepositoryCollections.length}
</span>
<span className="dropdown-caret"></span>
</summary>
<CollectionList />
<AddToCollections />
</details>
</div>
);
};
Loading

0 comments on commit e83ec15

Please sign in to comment.