-
Notifications
You must be signed in to change notification settings - Fork 9.2k
feat: emit metadata events for schema changes with actor context for webhooks #17622
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
Merged
Merged
Changes from 15 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
3fc1b1d
feat: add metadata eventing via webhooks
mabdullahabaid 776d643
feat: add userId and workspaceMemberId to metadata events for attribu…
mabdullahabaid 4d54144
Merge branch 'main' into metadata-events
mabdullahabaid 16a4343
chore: run the linter
mabdullahabaid 595180e
fix: use type instead of enum
mabdullahabaid 187c769
Merge branch 'main' into metadata-events
mabdullahabaid 3568b13
fix: add userId and workspaceMetadataId to metadata events for attrib…
mabdullahabaid 2769dca
fix: run the linter and resolve typecheck issues
mabdullahabaid 96fa444
Merge branch 'main' into metadata-events
FelixMalfait d4d57b8
fix: nested field metadata event not being fired on object creation
mabdullahabaid 9419a99
feat: ui changes for supporting metadata-events with webhooks
mabdullahabaid 4910188
Merge branch 'main' into metadata-events
FelixMalfait 04a70e1
fix: resolve issues with the the UI
mabdullahabaid bf7714a
fix: remove userId and workspaceMemberId drilling
mabdullahabaid 640d2cc
fix: ensure auth context is used to fetch userId
mabdullahabaid b476954
fix: resolve the issue with event not being sent over webhook for upd…
mabdullahabaid 7549286
chore: improve metadata events type system
mabdullahabaid 94a6390
chore: rename function
mabdullahabaid dccc5af
fix: imports should not be type
mabdullahabaid f143d7a
refactor(server): emitter service
prastoin f2a49e7
Merge remote-tracking branch 'origin/main' into metadata-events
prastoin fd6ab9d
feat(server): refactoring to be byUniversal compatible
prastoin b5d0d57
refactor(server): create
prastoin e019a94
feat(server): wip
prastoin afc5400
Merge branch 'main' into metadata-events
mabdullahabaid 34f805c
refactor(server): finalize rebase
prastoin f4bb67a
Merge remote-tracking branch 'origin/main' into metadata-events
prastoin a321ab0
Merge branch 'main' into metadata-events
mabdullahabaid 74dd8f0
feat: rework metadata-events with flat types and single source of tru…
mabdullahabaid c41d4c4
feat: rework metadata event types with distributive envelope and stri…
mabdullahabaid 20f4eb4
feat: add per-property typed update diffs with FlatEntityUpdateKey co…
mabdullahabaid 9b3413a
refactor(server): revert runner
prastoin 37b26e9
refactor
prastoin 8989755
chore
prastoin 4b3c447
fixup
prastoin 2c7fa97
fix
prastoin b0ba022
Merge branch 'main' into metadata-events
mabdullahabaid 7470551
feat: introduce CallMetadataWebhookJobData to preserve full MetadataE…
mabdullahabaid 78bf547
chore: run the linter
mabdullahabaid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
236 changes: 236 additions & 0 deletions
236
packages/twenty-front/src/modules/settings/developers/components/WebhookEntitySelect.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,236 @@ | ||
| import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; | ||
| import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; | ||
| import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent'; | ||
| import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; | ||
| import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; | ||
| import { DropdownMenuSectionLabel } from '@/ui/layout/dropdown/components/DropdownMenuSectionLabel'; | ||
| import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; | ||
| import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth'; | ||
| import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown'; | ||
| import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; | ||
| import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem'; | ||
| import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState'; | ||
| import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; | ||
| import { useTheme } from '@emotion/react'; | ||
| import styled from '@emotion/styled'; | ||
| import { t } from '@lingui/core/macro'; | ||
| import { useState } from 'react'; | ||
| import { isDefined } from 'twenty-shared/utils'; | ||
| import { | ||
| IconBox, | ||
| IconChevronDown, | ||
| IconCode, | ||
| IconEye, | ||
| IconNorthStar, | ||
| IconSettings, | ||
| IconTable, | ||
| useIcons, | ||
| } from 'twenty-ui/display'; | ||
| import { MenuItemSelect } from 'twenty-ui/navigation'; | ||
|
|
||
| const WEBHOOK_ENTITY_DROPDOWN_ID = 'webhook-entity-select'; | ||
|
|
||
| const StyledControlContainer = styled.div<{ disabled?: boolean }>` | ||
| align-items: center; | ||
| background-color: ${({ theme }) => theme.background.transparent.lighter}; | ||
| border: 1px solid ${({ theme }) => theme.border.color.medium}; | ||
| border-radius: ${({ theme }) => theme.border.radius.sm}; | ||
| box-sizing: border-box; | ||
| cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; | ||
| display: flex; | ||
| gap: ${({ theme }) => theme.spacing(1)}; | ||
| height: ${({ theme }) => theme.spacing(8)}; | ||
| justify-content: space-between; | ||
| padding: 0 ${({ theme }) => theme.spacing(2)}; | ||
| width: 100%; | ||
|
|
||
| &:hover { | ||
| background-color: ${({ theme, disabled }) => | ||
| disabled | ||
| ? theme.background.transparent.lighter | ||
| : theme.background.transparent.light}; | ||
| } | ||
| `; | ||
|
|
||
| const StyledControlLabel = styled.span` | ||
| color: ${({ theme }) => theme.font.color.primary}; | ||
| font-size: ${({ theme }) => theme.font.size.md}; | ||
| overflow: hidden; | ||
| text-overflow: ellipsis; | ||
| white-space: nowrap; | ||
| `; | ||
|
|
||
| const StyledControlIconChevronDown = styled(IconChevronDown)<{ | ||
| disabled?: boolean; | ||
| }>` | ||
| color: ${({ disabled, theme }) => | ||
| disabled ? theme.font.color.extraLight : theme.font.color.tertiary}; | ||
| `; | ||
|
|
||
| type WebhookEntitySelectProps = { | ||
| value: string | null; | ||
| onChange: (value: string | null) => void; | ||
| disabled?: boolean; | ||
| dropdownId?: string; | ||
| }; | ||
|
|
||
| export const WebhookEntitySelect = ({ | ||
| value, | ||
| onChange, | ||
| disabled = false, | ||
| dropdownId = WEBHOOK_ENTITY_DROPDOWN_ID, | ||
| }: WebhookEntitySelectProps) => { | ||
| const theme = useTheme(); | ||
| const [searchInput, setSearchInput] = useState(''); | ||
| const { objectMetadataItems } = useObjectMetadataItems(); | ||
| const { getIcon } = useIcons(); | ||
| const { closeDropdown } = useCloseDropdown(); | ||
|
|
||
| const selectedItemId = useRecoilComponentValue( | ||
| selectedItemIdComponentState, | ||
| dropdownId, | ||
| ); | ||
|
|
||
| const metadataOptions = [ | ||
| { label: t`All Metadata`, value: 'metadata.*', icon: IconNorthStar }, | ||
| { label: t`Object`, value: 'metadata.objectMetadata', icon: IconTable }, | ||
| { label: t`Field`, value: 'metadata.fieldMetadata', icon: IconBox }, | ||
| { label: t`View`, value: 'metadata.view', icon: IconEye }, | ||
| { label: t`View Field`, value: 'metadata.viewField', icon: IconEye }, | ||
| { label: t`Index`, value: 'metadata.index', icon: IconSettings }, | ||
| { label: t`Webhook`, value: 'metadata.webhook', icon: IconCode }, | ||
| ]; | ||
|
|
||
| const objectOptions = [ | ||
| { label: t`All Objects`, value: '*', Icon: IconNorthStar }, | ||
| ...objectMetadataItems.map((item) => ({ | ||
| label: item.labelPlural, | ||
| value: item.nameSingular, | ||
| Icon: getIcon(item.icon), | ||
| })), | ||
| ]; | ||
|
|
||
| const filteredObjectOptions = objectOptions.filter((option) => | ||
| option.label.toLowerCase().includes(searchInput.toLowerCase()), | ||
| ); | ||
|
|
||
| const filteredMetadataOptions = metadataOptions.filter((option) => | ||
| option.label.toLowerCase().includes(searchInput.toLowerCase()), | ||
| ); | ||
|
|
||
| // Find selected option label for display | ||
| const getSelectedLabel = () => { | ||
| if (!isDefined(value)) { | ||
| return t`Select entity`; | ||
| } | ||
| if (value === '*') { | ||
| return t`All Objects`; | ||
| } | ||
|
|
||
| const metadataOption = metadataOptions.find((opt) => opt.value === value); | ||
| if (isDefined(metadataOption)) { | ||
| return metadataOption.label; | ||
| } | ||
|
|
||
| const objectOption = objectOptions.find((opt) => opt.value === value); | ||
| if (isDefined(objectOption)) { | ||
| return objectOption.label; | ||
| } | ||
|
|
||
| return value; | ||
| }; | ||
|
|
||
| const handleSelect = (selectedValue: string) => { | ||
| if (disabled) return; | ||
| onChange(selectedValue); | ||
| closeDropdown(dropdownId); | ||
| setSearchInput(''); | ||
| }; | ||
|
|
||
| const shouldShowObjects = filteredObjectOptions.length > 0; | ||
| const shouldShowMetadata = filteredMetadataOptions.length > 0; | ||
| const shouldShowSeparator = shouldShowObjects && shouldShowMetadata; | ||
|
|
||
| const selectableItemIds = [ | ||
| ...filteredObjectOptions.map((opt) => opt.value), | ||
| ...filteredMetadataOptions.map((opt) => opt.value), | ||
| ]; | ||
|
|
||
| return ( | ||
| <Dropdown | ||
| dropdownId={dropdownId} | ||
| dropdownPlacement="bottom-start" | ||
| disableClickForClickableComponent={disabled} | ||
| onClose={() => setSearchInput('')} | ||
| clickableComponent={ | ||
| <StyledControlContainer disabled={disabled}> | ||
| <StyledControlLabel>{getSelectedLabel()}</StyledControlLabel> | ||
| <StyledControlIconChevronDown | ||
| disabled={disabled} | ||
| size={theme.icon.size.md} | ||
| /> | ||
| </StyledControlContainer> | ||
| } | ||
| dropdownComponents={ | ||
| <DropdownContent widthInPixels={GenericDropdownContentWidth.Medium}> | ||
| <DropdownMenuSearchInput | ||
| autoFocus | ||
| value={searchInput} | ||
| placeholder={t`Search...`} | ||
| onChange={(event) => setSearchInput(event.target.value)} | ||
| /> | ||
| <DropdownMenuSeparator /> | ||
| <SelectableList | ||
| selectableListInstanceId={dropdownId} | ||
| selectableItemIdArray={selectableItemIds} | ||
| focusId={dropdownId} | ||
| > | ||
| <DropdownMenuItemsContainer hasMaxHeight> | ||
| {shouldShowObjects && ( | ||
| <> | ||
| <DropdownMenuSectionLabel label={t`Core Objects`} /> | ||
| {filteredObjectOptions.map((option) => ( | ||
| <SelectableListItem | ||
| key={option.value} | ||
| itemId={option.value} | ||
| onEnter={() => handleSelect(option.value)} | ||
| > | ||
| <MenuItemSelect | ||
| LeftIcon={option.Icon} | ||
| text={option.label} | ||
| selected={value === option.value} | ||
| focused={selectedItemId === option.value} | ||
| onClick={() => handleSelect(option.value)} | ||
| /> | ||
| </SelectableListItem> | ||
| ))} | ||
| </> | ||
| )} | ||
| {shouldShowSeparator && <DropdownMenuSeparator />} | ||
| {shouldShowMetadata && ( | ||
| <> | ||
| <DropdownMenuSectionLabel label={t`Metadata`} /> | ||
| {filteredMetadataOptions.map((option) => ( | ||
| <SelectableListItem | ||
| key={option.value} | ||
| itemId={option.value} | ||
| onEnter={() => handleSelect(option.value)} | ||
| > | ||
| <MenuItemSelect | ||
| LeftIcon={option.icon} | ||
| text={option.label} | ||
| selected={value === option.value} | ||
| focused={selectedItemId === option.value} | ||
| onClick={() => handleSelect(option.value)} | ||
| /> | ||
| </SelectableListItem> | ||
| ))} | ||
| </> | ||
| )} | ||
| </DropdownMenuItemsContainer> | ||
| </SelectableList> | ||
| </DropdownContent> | ||
| } | ||
| /> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.