Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: unstrike all on list #47

Merged
merged 2 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"asynciterable",
"cacheable",
"codegen",
"Cooldown",
"Dokku",
"listtogether",
"precaching",
Expand Down
6 changes: 3 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
List Together is live at https://listtogether.app
# TODO

I will be making edits and bug fixes based on our personal use and any user feedback we receive. Below is a list of features/fixes I would like implemented if I spend more time on the project.
List Together is live at <https://listtogether.app>

## **🤔 Possibly Todo**
Below is a list of features/fixes we would like implemented if we spend more time on this project

- Strike notes individually, rather than with item
- Share list via link or email. Shared user would still need to create account upon clicking share link.
Expand Down
2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "list-together-graphql-gateway",
"version": "1.3.5",
"version": "1.4.0",
"author": "Derek R. Sonnenberg <[email protected]>",
"license": "MIT",
"main": "server.ts",
Expand Down
17 changes: 10 additions & 7 deletions server/src/resolvers/item/strikeItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,15 @@ export class StrikeItemResolver {
}
itemRemovalCallback(userToListTable, item.name);
} else {
// Sort unstriked items back into the list
// Sort un-striked items back into the list
if (userToListTable.sortedItems) {
const sortedListWithoutUnstrikedItem = userToListTable.sortedItems.filter(
(i) => i !== item.name
);
userToListTable.sortedItems = sortedListWithoutUnstrikedItem;
const sortedListWithoutUnStrikedItem =
userToListTable.sortedItems.filter((i) => i !== item.name);
userToListTable.sortedItems = sortedListWithoutUnStrikedItem;

userToListTable.sortedItems = sortIntoList(userToListTable, item.name);
}
// Item was unstriked -- remove it from removalArray if found
// Item was un-striked -- remove it from removalArray if found
if (userToListTable.removedItems?.includes(item.name)) {
userToListTable.removedItems = userToListTable.removedItems.filter(
(i) => i !== item.name
Expand All @@ -92,7 +91,11 @@ export class StrikeItemResolver {
}

await userToListTable.save();
strikeOnSharedLists(userToListTable, item.name, item.strike, publish);
if (item.strike) {
strikeOnSharedLists(userToListTable, [item.name], [], publish);
} else {
strikeOnSharedLists(userToListTable, [], [item.name], publish);
}
return { userToList: [userToListTable] };
}
}
77 changes: 77 additions & 0 deletions server/src/resolvers/item/strikeItems.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { logger } from '../../middleware/logger';
import { MyContext } from '../../MyContext';
import {
Arg,
Ctx,
Mutation,
Publisher,
PubSub,
Resolver,
UseMiddleware
} from 'type-graphql';
import { StrikeItemsInput } from '../types/input/StrikeItemsInput';
import { UserToListResponse } from '../types/response/UserToListResponse';
import { SubscriptionPayload } from '../types/subscription/SubscriptionPayload';
import { Topic } from '../types/subscription/SubscriptionTopics';
import { getUserListTable } from '../../services/list/getUserListTable';
import { strikeOnSharedLists } from '../../services/item/strikeOnSharedLists';
import { FieldError } from '../types/response/FieldError';

@Resolver()
export class StrikeItemsResolver {
// Style items on list
@UseMiddleware(logger)
@Mutation(() => UserToListResponse)
async strikeItems(
@Arg('data') { listId, itemNameArray }: StrikeItemsInput,
@Ctx() context: MyContext,
@PubSub(Topic.updateList) publish: Publisher<SubscriptionPayload>
): Promise<UserToListResponse> {
const getListPayload = await getUserListTable({
context,
listId,
relations: ['list', 'list.items', 'itemHistory'],
validatePrivilege: 'strike',
validateItemsExist: true
});
if (getListPayload.errors) return { errors: getListPayload.errors };
const userToListTable = getListPayload.userToList![0];

const errors: FieldError[] = [];
const strikedItems: string[] = [];
const unStrikedItems: string[] = [];

for (const itemName of itemNameArray) {
const item = userToListTable.list.items!.find(
({ name }) => name === itemName
);
if (!item) {
errors.push({
field: 'name',
message: `Item
"${item}" does not exist on list...`
});
continue;
}

// Strike the item
item.strike = !item.strike;
if (item.strike) {
strikedItems.push(itemName);
} else {
unStrikedItems.push(itemName);
}
}
userToListTable.sortedItems = [
...unStrikedItems,
...(userToListTable.sortedItems?.filter(
(i) => ![...strikedItems, ...unStrikedItems].includes(i)
) || []),
...strikedItems
];

await userToListTable.save();
strikeOnSharedLists(userToListTable, strikedItems, unStrikedItems, publish);
return { userToList: [userToListTable] };
}
}
9 changes: 9 additions & 0 deletions server/src/resolvers/types/input/StrikeItemsInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Field, InputType } from 'type-graphql';
@InputType()
export class StrikeItemsInput {
@Field()
listId: string;

@Field(() => [String])
itemNameArray: string[];
}
33 changes: 18 additions & 15 deletions server/src/services/item/strikeOnSharedLists.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { SubscriptionPayload } from '../../resolvers/types/subscription/SubscriptionPayload';
import { UserToList } from '../../entities';
import { getSharedListTables } from '../list/getSharedListTables';
import { sortIntoList } from './sortIntoList';

export const strikeOnSharedLists = async (
userToList: UserToList,
itemName: string,
strike: boolean,
strikedItems: string[],
unStrikedItems: string[],
publish: (payload: SubscriptionPayload) => Promise<void>
) => {
const sharedUserToListTables = await getSharedListTables(userToList);
Expand All @@ -18,27 +17,31 @@ export const strikeOnSharedLists = async (
if (!table.sortedItems) {
console.error('Shared list has no sortedItems..');
} else {
if (strike) {
table.sortedItems = [
...table.sortedItems.filter((i) => i !== itemName),
itemName
];
} else {
table.sortedItems = table.sortedItems.filter((i) => i !== itemName);
table.sortedItems = sortIntoList(table, itemName);
}
table.sortedItems = [
...unStrikedItems,
...(table.sortedItems?.filter(
(i) => ![...strikedItems, ...unStrikedItems].includes(i)
) || []),
...strikedItems
];
await table.save();
}
})
);

const allItems = [...strikedItems, ...unStrikedItems];
await publish({
updatedListId: userToList.listId,
/** Don't notify user who striked the item */
userIdToExclude: userToList.userId,
notification: `'${itemName}' has been ${
strike ? 'striked' : 'un-striked'
} on list '${userToList.list.title}'.`
notification:
allItems.length === 1
? `'${allItems[0]}' has been ${
userToList.list.items?.find((i) => i.name === allItems[0])!.strike
? 'striked'
: 'un-striked'
} on list '${userToList.list.title}'.`
: `Items have been striked on list '${userToList.list.title}'.`
});
}
};
2 changes: 2 additions & 0 deletions server/src/utils/createSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { EditNoteResolver } from '../resolvers/item/editNote';

// Redis PubSub
import { pubSub } from './pubSub';
import { StrikeItemsResolver } from '../resolvers/item/strikeItems';

// // ApolloServer PubSub
// import { PubSub } from 'apollo-server-express';
Expand All @@ -45,6 +46,7 @@ export const createSchema = () =>
AddItemResolver,
DeleteItemsResolver,
StrikeItemResolver,
StrikeItemsResolver,
AddNoteResolver,
DeleteNoteResolver,
DeleteAccountResolver,
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "list-together-pwa",
"version": "1.0.1",
"version": "1.1.0",
"author": "Derek R. Sonnenberg <[email protected]>",
"license": "MIT",
"devDependencies": {
Expand Down
60 changes: 59 additions & 1 deletion web/src/components/sideMenu/RedoButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
useEditNoteMutation,
useSortItemsMutation,
useSortListsMutation,
useStrikeItemMutation
useStrikeItemMutation,
useStrikeItemsMutation
} from 'src/generated/graphql';
import useDelayedFunction from 'src/hooks/useDelayedFunction';
import useKeyPress from 'src/hooks/useKeyPress';
Expand Down Expand Up @@ -120,6 +121,17 @@ export default function RedoButton() {
);
break;

case 'strikeItems':
redoWithMutation = (
<WithStrikeItems
nextRedo={nextRedo}
dispatch={dispatch}
keyboardSubmit={redoKeyboardButton}
useKeyCooldown={useKeyCooldown}
/>
);
break;

case 'editItemName':
redoWithMutation = (
<WithEditItemName
Expand Down Expand Up @@ -475,6 +487,52 @@ function WithStrikeItem({
);
}

function WithStrikeItems({
nextRedo,
dispatch,
keyboardSubmit,
useKeyCooldown
}: WithMutationProps) {
const [mutationSubmitting, setMutationSubmitting] = useState(false);
const mutationCooldown = useDelayedFunction(() => {
setMutationSubmitting(false);
});
const [strikeItem, { loading }] = useStrikeItemsMutation();
if (nextRedo[0] !== 'strikeItems') return null;
const { listId, itemNameArray } = nextRedo[1];
const handleMutation = async () => {
if (loading || mutationSubmitting) return;
setMutationSubmitting(true);
const { data } = await strikeItem({
variables: { data: { listId, itemNameArray } }
});
const errors = data?.strikeItems.errors;
if (errors) {
console.log(errors);
sendNotification(dispatch, [
'Could not complete Redo action, that item no longer exists on this list..'
]);
dispatch({ type: 'REMOVE_REDO' });
mutationCooldown(500); // .5 sec delay
} else {
dispatch({ type: 'REDO_MUTATION' });
mutationCooldown(500);
}
};
useEffect(() => {
if (keyboardSubmit && !mutationSubmitting) {
useKeyCooldown();
handleMutation();
}
}, [keyboardSubmit]);
return (
<RedoButtonInner
useMutationHook={handleMutation}
mutationSubmitting={mutationSubmitting}
/>
);
}

function WithEditItemName({
nextRedo,
dispatch,
Expand Down
23 changes: 21 additions & 2 deletions web/src/components/sideMenu/SideMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import AddItemIcon from '../svg/sideMenu/AddItemIcon';
import ReviewListIcon from '../svg/sideMenu/ReviewListIcon';
import RedoButton from './RedoButton';
import UndoButton from './UndoButton';
import useStrikeItems from '../../hooks/mutations/item/useStrikeItems';

type SideMenuProps = {
strikedItems: string[];
Expand All @@ -25,12 +26,18 @@ export default function SideMenu({ strikedItems }: SideMenuProps) {
};

const [deleteItems, deleteItemsSubmitting] = useDeleteItems();
/** Use `deleteItems` mutation on all striked items */
const handleDeleteAllClick = () => {
if (deleteItemsSubmitting) return;
deleteItems(strikedItems);
};

const [strikeItems, strikeItemsSubmitting] = useStrikeItems();
const handleUnStrikeAllClick = async () => {
if (strikeItemsSubmitting) return;
await strikeItems(strikedItems);
handleReturnClick();
};

const handleReviewClick = () => {
dispatch({
type: 'SET_SIDE_MENU_STATE',
Expand Down Expand Up @@ -73,7 +80,12 @@ export default function SideMenu({ strikedItems }: SideMenuProps) {
...keysToHandle,
{ keyValues: ['d'], callback: () => handleDeleteAllClick() }
];
keysToHandle = [
...keysToHandle,
{ keyValues: ['u'], callback: () => handleUnStrikeAllClick() }
];
}

if (hasStrikedItems && userCanDelete) {
keysToHandle = [
...keysToHandle,
Expand All @@ -96,11 +108,18 @@ export default function SideMenu({ strikedItems }: SideMenuProps) {
{/** Review strikes mode */}
<IconButton
icon={<DeleteIcon />}
ariaLabel="Delate All Striked Items"
ariaLabel="Delete All Striked Items"
onClick={handleDeleteAllClick}
text={`Delete All${largeScreen ? ' (D)' : ''}`}
style={style}
/>
<IconButton
icon={<DeleteIcon />}
ariaLabel="Un-strike All Striked Items"
onClick={handleUnStrikeAllClick}
text={`Un-strike All${largeScreen ? ' (U)' : ''}`}
style={style}
/>
<IconButton
icon={<ReviewListIcon />}
ariaLabel="Return to List"
Expand Down
Loading
Loading