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

626 save advanced searches #629

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
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: 0 additions & 1 deletion apps/backoffice/src/graphql/generated/schema-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,6 @@ export type MutationDeleteOrganizationArgs = {

export type MutationDeleteSearchQueryStringArgs = {
id: Scalars['String']['input'];
isOrganization: Scalars['Boolean']['input'];
};

export type MutationEmulateAdminArgs = {
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/schema/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ type Mutation {
"""
deleteInvitationLink(role: OrganizationRoleEnum!): Boolean!
deleteOrganization(organizationId: String): Organization!
deleteSearchQueryString(id: String!, isOrganization: Boolean!): SearchQueryString!
deleteSearchQueryString(id: String!): SearchQueryString!
emulateAdmin(organizationId: String!): Tokens!
fillMetaCreatives(integrationIds: [String!]): Boolean!
forgetPassword(email: String!): Boolean!
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,12 @@ builder.mutationFields((t) => ({
nullable: false,
args: {
id: t.arg.string({ required: true }),
isOrganization: t.arg.boolean({ required: true }),
},
resolve: async (query, _root, args, ctx, _info) => {
if (args.isOrganization) {
const searchQuery = await prisma.searchQueryString.findUniqueOrThrow({
where: { id: args.id },
});
if (searchQuery.isOrganization) {
const userOrganization = await prisma.userOrganization.findUniqueOrThrow({
where: { userId_organizationId: { userId: ctx.currentUserId, organizationId: ctx.organizationId } },
});
Expand Down
20 changes: 18 additions & 2 deletions apps/web/messages/el.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,23 @@
"addSearchTerm": "Προσθήκη παραμέτρου αναζήτησης",
"addSearchSubTerm": "Προσθήκη υποόρων",
"searchTermsHint": "Ξεκινήστε να προσθέτετε όρους αναζήτησης για να προσαρμόσετε την αναζήτησή σας.",
"clearSearch": "Εκκαθάριση αναζήτησης"
"clearSearch": "Εκκαθάριση αναζήτησης",
"searchPresets": "Αποθηκευμένες Αναζητήσεις",
"searchPresetHint": "Επιλέξτε αποθηκευμένες αναζητήσεις",
"saveSearchPresetTitle": "Αποθήκευση αναζήτησης",
"deleteSearchPresetTitle": "Διαγραφή αναζήτησης",
"presetName": "Όνομα Αναζήτησης",
"saveDescription": "Πρόκειται να αποθηκεύσετε αυτήν τη ρύθμιση αναζήτησης. Αποφασίστε αν θέλετε να αποθηκευτεί για ολόκληρο τον οργανισμό ή μόνο για εσάς.",
"deleteDescription": "Πρόκειται να διαγράψετε αυτήν τη ρύθμιση προχωρημένης αναζήτησης. Επιβεβαιώστε την ενέργειά σας για να συνεχίσετε.",
"updateSelectedSearch": "Ενημέρωση επιλεγμένης αναζήτησης",
"saveAsNewUser": "Αποθήκευση ως νέα μόνο για μένα",
"saveAsNewOrg": "Αποθήκευση ως νέα για τον οργανισμό",
"groupUser": "Χρήστης",
"groupOrganization": "Οργανισμός",
"save": "Αποθήκευση",
"saveTooltip": "Ενημέρωση / Αποθήκευση ως νέο",
"delete": "Διαγραφή",
"cancel": "Άκυρο"
}
},
"summary": {
Expand Down Expand Up @@ -206,4 +222,4 @@
"roleUser": "Μέλος",
"submit": "Υποβολή"
}
}
}
20 changes: 18 additions & 2 deletions apps/web/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,23 @@
"addSearchTerm": "Add search term",
"addSearchSubTerm": "Add subterm",
"searchTermsHint": "Start adding search terms to customize your search.",
"clearSearch": "Clear search"
"clearSearch": "Clear search",
"searchPresets": "Search Preset",
"searchPresetHint": "Select a search preset",
"saveSearchPresetTitle": "Save Search Preset",
"deleteSearchPresetTitle": "Delete Search Preset",
"presetName": "Preset Name",
"saveDescription": "You're about to save this search configuration. Please decide if you want it saved for the entire organization or just you.",
"deleteDescription": "You're about to erase this advanced search configuration. Please confirm your action to proceed.",
"updateSelectedSearch": "Update selected search",
"saveAsNewUser": "Save as new only for me",
"saveAsNewOrg": "Save as new for organization",
"groupUser": "User",
"groupOrganization": "Organization",
"save": "Save",
"saveTooltip": "Update / Save as new",
"delete": "Delete",
"cancel": "Cancel"
}
},
"summary": {
Expand Down Expand Up @@ -206,4 +222,4 @@
"roleUser": "Member",
"submit": "Submit"
}
}
}
20 changes: 18 additions & 2 deletions apps/web/messages/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,23 @@
"addSearchTerm": "Zoekparameter toevoegen",
"addSearchSubTerm": "Subterm toevoegen",
"searchTermsHint": "Begin met het toevoegen van zoektermen om uw zoekopdracht te personaliseren.",
"clearSearch": "Zoekopdracht wissen"
"clearSearch": "Zoekopdracht wissen",
"searchPresets": "Opgeslagen Zoekopdrachten",
"searchPresetHint": "Kies opgeslagen zoekopdrachten",
"saveSearchPresetTitle": "Zoekopdracht opslaan",
"deleteSearchPresetTitle": "Zoekopdracht verwijderen",
"presetName": "Naam van Zoekopdracht",
"saveDescription": "U staat op het punt deze zoekinstelling op te slaan. Beslis of u deze wilt opslaan voor de hele organisatie of alleen voor uzelf.",
"deleteDescription": "U staat op het punt deze geavanceerde zoekinstelling te verwijderen. Bevestig uw actie om door te gaan.",
"updateSelectedSearch": "Geselecteerde zoekopdracht bijwerken",
"saveAsNewUser": "Opslaan als nieuw alleen voor mij",
"saveAsNewOrg": "Opslaan als nieuw voor de organisatie",
"groupUser": "Gebruiker",
"groupOrganization": "Organisatie",
"save": "Opslaan",
"saveTooltip": "Bijwerken / Opslaan als nieuw",
"delete": "Verwijderen",
"cancel": "Annuleren"
}
},
"summary": {
Expand Down Expand Up @@ -206,4 +222,4 @@
"roleUser": "Lid",
"submit": "Indienen"
}
}
}
19 changes: 19 additions & 0 deletions apps/web/src/app/(authenticated)/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import {
type AdAccountsQuery,
type MeQuery,
type RefreshTokenQuery,
type SearchQueryStringsQuery,
type MutationUpsertSearchQueryStringArgs,
type UpsertSearchQueryStringMutation,
type MutationDeleteSearchQueryStringArgs,
type DeleteSearchQueryStringMutation,
} from '@/graphql/generated/schema-server';
import { urqlClientSdk, urqlClientSdkRefresh } from '@/lib/urql/urql-client';
import { handleUrqlRequest, type UrqlResult } from '@/util/handle-urql-request';
Expand All @@ -21,3 +26,17 @@ export const refreshJWTToken = async (): Promise<RefreshTokenQuery> => await urq
export const sendFeedback = async (
values: SendFeedbackMutationVariables,
): Promise<UrqlResult<SendFeedbackMutation, string>> => await handleUrqlRequest(urqlClientSdk().sendFeedback(values));

export const getSearchQueryStrings = async (): Promise<UrqlResult<SearchQueryStringsQuery>> => {
return await handleUrqlRequest(urqlClientSdk().searchQueryStrings());
};

export const upsertSearchQueryString = async (
values: MutationUpsertSearchQueryStringArgs,
): Promise<UrqlResult<UpsertSearchQueryStringMutation, string>> =>
await handleUrqlRequest(urqlClientSdk().upsertSearchQueryString(values));

export const deleteSearchQueryString = async (
values: MutationDeleteSearchQueryStringArgs,
): Promise<UrqlResult<DeleteSearchQueryStringMutation, string>> =>
await handleUrqlRequest(urqlClientSdk().deleteSearchQueryString(values));
4 changes: 4 additions & 0 deletions apps/web/src/app/atoms/searches-atoms.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { atom } from 'jotai';
import { type SearchQueryStringsQuery } from '@/graphql/generated/schema-server';

export const searchesAtom = atom<SearchQueryStringsQuery['searchQueryStrings']>([]);
11 changes: 7 additions & 4 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { SpeedInsights } from '@vercel/speed-insights/next';
import { getMessages } from 'next-intl/server';
import { headers } from 'next/headers';
import { GoogleAnalytics } from '@next/third-parties/google';
import { ModalsProvider } from '@mantine/modals';
import NotificationsHandler from '@/components/misc/notifications-handler';
import { env } from '@/env.mjs';

Expand All @@ -32,10 +33,12 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<ColorSchemeScript />
<NextIntlClientProvider messages={messages} locale={preferredLocale}>
<MantineProvider defaultColorScheme="auto">
<NotificationsHandler />
{children}
<Analytics />
<SpeedInsights />
<ModalsProvider>
<NotificationsHandler />
{children}
<Analytics />
<SpeedInsights />
</ModalsProvider>
</MantineProvider>
</NextIntlClientProvider>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { OrganizationRoleEnum, type UserRolesInput } from '@/graphql/generated/s
import { organizationAtom } from '@/app/atoms/organization-atoms';
import getOrganization from '@/app/(authenticated)/organization/actions';
import { userDetailsAtom } from '@/app/atoms/user-atoms';
import { type DropdownValueType } from '@/util/types';

interface PropsType {
setNewUsers: (users: UserRolesInput[]) => void;
Expand All @@ -22,12 +23,6 @@ interface SelectedUsersType {
key: string;
}

interface DropdownValueType {
label: string;
value: string;
disabled: boolean;
}

const INITIAL_USER_VALUE: SelectedUsersType = {
role: OrganizationRoleEnum.ORG_MEMBER,
userId: null,
Expand Down
43 changes: 43 additions & 0 deletions apps/web/src/components/search/saved-searches/delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use client';

import { ActionIcon, Text, Tooltip } from '@mantine/core';
import { modals } from '@mantine/modals';
import { IconTrash } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';

interface PropsType {
canUserAlter: boolean;
selectedSearchID: string | null;
isPending: boolean;
handleDelete: () => void;
}

export default function Delete(props: PropsType): React.ReactNode {
const tSearch = useTranslations('insights.search');

const openModal = (): void => {
modals.openConfirmModal({
title: tSearch('deleteSearchPresetTitle'),
children: <Text size="sm">{tSearch('deleteDescription')}</Text>,
labels: { confirm: tSearch('delete'), cancel: tSearch('cancel') },
confirmProps: { loading: props.isPending, color: 'red' },
onConfirm: () => {
props.handleDelete();
},
});
};

return (
<Tooltip label={tSearch('delete')}>
<ActionIcon
disabled={props.isPending || !props.selectedSearchID || !props.canUserAlter}
color="red.4"
variant="outline"
size={34}
onClick={openModal}
>
<IconTrash />
</ActionIcon>
</Tooltip>
);
}
115 changes: 115 additions & 0 deletions apps/web/src/components/search/saved-searches/save.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
'use client';

import { ActionIcon, Flex, Radio, Text, TextInput, Tooltip } from '@mantine/core';
import { useForm, zodResolver } from '@mantine/form';
import { modals } from '@mantine/modals';
import { IconDeviceFloppy } from '@tabler/icons-react';
import { useTranslations } from 'next-intl';
import { useEffect, useRef } from 'react';
import { z } from 'zod';

interface PropsType {
canUserAlter: boolean;
canSaveAsOrg: boolean;
selectedSearchID: string | null;
selectedSearchName: string;
isSelectedSearchOrganization: boolean;
isPending: boolean;
handleSave: (name: string, isOrganization: boolean, id: string | null) => void;
}

enum SaveTypes {
Update = 'Update',
SaveAsNew = 'SaveAsNew',
SaveAsNewForOrg = 'SaveAsNewForOrg',
}

const ValidationSchema = z.object({
name: z.string().min(1, { message: 'Name is required' }),
});

export default function Save(props: PropsType): React.ReactNode {
const tSearch = useTranslations('insights.search');
const nameRef = useRef<HTMLInputElement>(null);

const form = useForm({
mode: 'uncontrolled',
initialValues: {
name: '',
saveType: SaveTypes.SaveAsNew,
},
validate: zodResolver(ValidationSchema),
});

// Synchronize form name with selectedSearchName whenever props.selectedSearchName changes
useEffect(() => {
form.setFieldValue('name', props.selectedSearchName);
}, [form, props.selectedSearchName]);

const getFormNameValue = (): string => {
if (!nameRef.current) return '';
return nameRef.current.value;
};

const openModal = (): void => {
modals.openConfirmModal({
title: tSearch('saveSearchPresetTitle'),
children: (
<form
onSubmit={(e) => {
e.preventDefault();
}}
>
<Flex direction="column" gap="sm">
<TextInput
description={tSearch('presetName')}
placeholder={tSearch('presetName')}
mb="sm"
key={form.key('name')}
{...form.getInputProps('name')}
ref={nameRef}
/>
<Text size="sm">{tSearch('saveDescription')}</Text>
<Radio.Group key={form.key('saveType')} {...form.getInputProps('saveType')}>
<Flex direction="column" gap="sm">
<Radio
disabled={!props.selectedSearchID || !props.canUserAlter}
value={SaveTypes.Update}
label={tSearch('updateSelectedSearch')}
/>
<Radio value={SaveTypes.SaveAsNew} label={tSearch('saveAsNewUser')} />
<Radio
value={SaveTypes.SaveAsNewForOrg}
label={tSearch('saveAsNewOrg')}
disabled={!props.canSaveAsOrg}
/>
</Flex>
</Radio.Group>
</Flex>
</form>
),
labels: { confirm: tSearch('save'), cancel: tSearch('cancel') },
confirmProps: { loading: props.isPending },
onCancel: () => {
form.reset();
},
onConfirm: () => {
const name = getFormNameValue();
const values = form.getValues();
let isOrganization = values.saveType === SaveTypes.SaveAsNewForOrg;
if (values.saveType === SaveTypes.Update) isOrganization = props.isSelectedSearchOrganization;
const idToUpdate = values.saveType === SaveTypes.Update ? props.selectedSearchID : null;
props.handleSave(name, isOrganization, idToUpdate);
form.reset();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when the type is Update, we may want to order the searches again, since this is the current behavior. For example if I rename aaa to zzz, I would expect the updated entry to be on the bottom (which it does if you refresh)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem to behave that way (this is the fetched data withou updating).

image

},
});
};

return (
<Tooltip label={tSearch('saveTooltip')}>
<ActionIcon disabled={props.isPending} variant="outline" size={34} onClick={openModal}>
<IconDeviceFloppy />
</ActionIcon>
</Tooltip>
);
}
Loading