Skip to content

Commit

Permalink
♻️ front: moved driveitem access entities editing to a helper file (#50)
Browse files Browse the repository at this point in the history
  • Loading branch information
ericlinagora committed May 13, 2024
1 parent ecf106c commit fc3ebaa
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 131 deletions.
4 changes: 3 additions & 1 deletion tdrive/frontend/src/app/features/drive/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export type DriveItem = {
scope: string;
};

export type DriveFileAccessLevelForInherited = 'none' | 'manage';
export type DriveFileAccessLevelForPublicLink = 'none' | 'read' | 'write';
export type DriveFileAccessLevel = 'none' | 'read' | 'write' | 'manage';

export type DriveItemAccessInfo = {
Expand All @@ -50,7 +52,7 @@ export type DriveItemAccessInfo = {
entities: AuthEntity[];
};

type AuthEntity = {
export type AuthEntity = {
type: 'user' | 'channel' | 'company' | 'folder';
id: string | 'parent';
level: DriveFileAccessLevel;
Expand Down
167 changes: 167 additions & 0 deletions tdrive/frontend/src/app/features/files/utils/access-info-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
AuthEntity,
DriveItemAccessInfo,
DriveFileAccessLevel,
DriveFileAccessLevelForInherited,
DriveFileAccessLevelForPublicLink,
DriveItem,
} from '@features/drive/types';

const entityMatcher = (entityType: AuthEntity['type'], entityId?: string) =>
(entity: AuthEntity) =>
entity && entity.type === entityType && (entityId == undefined || entity.id === entityId);

/** Updates the level of entities matching the given type and id, or adds the return
* of updater with no argument.
*
* If an item is not unique, all are modified and a console warning is printed except for `entityType` "channel".
*/
function upsertEntities(
entities: AuthEntity[] | undefined,
entityType: AuthEntity['type'],
entityId: string | undefined,
level: DriveFileAccessLevel,
) {
const updater = (existing?: AuthEntity) => {
if (!existing && !entityId) throw new Error("Cannot create entry without entityId");
return {
...(existing || { type: entityType, id: entityId }),
level,
};
};
let found = false;
if (!entities || entities.length == 0) return [updater()];
const matcher = entityMatcher(entityType, entityId);
const mapped = entities.map(item => {
if (!matcher(item)) return item;
if (found && entityType != "channel") console.warn(`DriveItem has more than one access_info entry for '${entityType}' id = ${entityId}:`, item);
found = true;
return updater(item);
}).filter(x => x);
if (found) return mapped;
return [...entities, updater()];
}

/** If `level == false`, deletes matching entries, otherwise calls `upsertEntities`
*/
function editEntities(
entities: AuthEntity[] | undefined,
entityType: AuthEntity['type'],
entityId: string | undefined,
level: DriveFileAccessLevel | false,
) {
if (level) return upsertEntities(entities, entityType, entityId, level);
const matcher = entityMatcher(entityType, entityId);
return entities && entities.filter(item => !matcher(item));
}

/** Return a list of all DriveItemAccessInfo for entities with the provided type sorted by id */
const accessInfosOfEntitiesOfType = (item: DriveItem | undefined, entityType: AuthEntity['type']) =>
item?.access_info.entities.filter(entityMatcher(entityType)).sort((a, b) => a.id.localeCompare(b.id)) || [];

/** Find access level for the given entity in the DriveItem's access_info and return it's level if any, or undefined */
export function accessLevelOfEntityForItem(
item: DriveItem | undefined,
entityType: AuthEntity['type'],
entityId: string,
) {
const matcher = entityMatcher(entityType, entityId);
return item?.access_info.entities.filter(matcher)[0]?.level;
}

/** Return the item with the access right for the given entity added or changed to level; or if its false, remove if any */
const itemWithEntityAccessChanged = (
item: DriveItem,
entityType: AuthEntity['type'],
entityId: string | undefined,
level: DriveFileAccessLevel | false,
) => ({
...item,
access_info: {
...item.access_info,
entities: editEntities(item.access_info.entities, entityType, entityId, level),
},
} as DriveItem);

// Note this just assumes uniqueness and just logs in the console if that is not the case and uses the first one.
const getAccessLevelOfUniqueForType = (item: DriveItem | undefined, entityType: AuthEntity['type'], entityId?: string) => {
if (!item) return undefined;
const accesses = item.access_info.entities.filter(entityMatcher(entityType, entityId));
if (accesses.length != 1 && entityType != "channel")
console.warn(`DriveItem doesn't have exactly one access_info entry for '${entityType}${entityId ? " id: " + entityId : ""}':`, item);
return accesses[0]?.level;
}

/** Return a shallow copy of item with the access right for the given user added or changed to level; or if its false, removed */
export const changeUserAccess = (item: DriveItem, userId: string, level: DriveFileAccessLevel | false) =>
itemWithEntityAccessChanged(item, "user", userId, level);

/** Return a list of all DriveItemAccessInfo for users sorted by id */
export const getAllUserAccesses = (item: DriveItem) => accessInfosOfEntitiesOfType(item, "user");

/** Return the access level for the provided user; an entry is expected to exist (or there is a `console.warn`) */
export const getUserAccessLevel = (item: DriveItem, userId: string) => getAccessLevelOfUniqueForType(item, "user", userId);



/** Return current access level inherited from parent folder by item */
export const getInheritedAccessLevel = (item?: DriveItem) => getAccessLevelOfUniqueForType(item, "folder");

/** Return a shallow copy of item with the access right for the given entity added or changed to level; or if its false, removed */
export const changeInheritedAccess = (item: DriveItem, level: DriveFileAccessLevelForInherited | false) =>
itemWithEntityAccessChanged(item, "folder", "parent", level);



/** Return access level for the company type entity if any */
export const getCompanyAccessLevel = (item?: DriveItem) => getAccessLevelOfUniqueForType(item, "company");

/** Return a shallow copy of item with the access right for the given entity changed to level; or if its false, removed.
* Removing is irreversible. Calling this method setting a level on a `DriveItem` that doesn't have a company entry will
* throw an Error.
*/
export const changeCompanyAccessLevel = (item: DriveItem, level: DriveFileAccessLevel | false) =>
itemWithEntityAccessChanged(item, "company", undefined, level);



/** Return the first, if any, access level of a channel */
export const getFirstChannelAccessLevel = (item?: DriveItem) => getAccessLevelOfUniqueForType(item, "channel");

/** Return a shallow copy of item with the access right for the given entity changed to level; or if its false, removed.
* Removing is irreversible. Calling this method setting a level on a `DriveItem` that doesn't have a channel entry will
* throw an Error.
*/
export const changeAllChannelAccessLevels = (item: DriveItem, level: DriveFileAccessLevel | false) =>
itemWithEntityAccessChanged(item, "channel", undefined, level);



/** Return a shallow copy of item with the public link properties for the given entity changed
*
* - level defaults to 'none' if not set and this is called without a value in `changes` for it
* - sets all fields in `changes` argument as they are, even if their value is null, undefined, false etc
* - runtime does not validate that fields in `changes` argument are properly typed or correctly named
*/
export function changePublicLink(
item: DriveItem,
changes: {
level?: DriveFileAccessLevelForPublicLink,
expiration?: number,
password?: string,
},
) {
const publicSettings : DriveItemAccessInfo["public"] = item.access_info.public ? { ...item.access_info.public } : { token: '', level: "none" };
Object.entries(changes).forEach(([field, value]) => (publicSettings as { [k: string]: unknown })[field] = value);
return {
...item,
access_info: {
...item.access_info,
public: publicSettings,
},
} as DriveItem;
}

/** Return the access level of the public link if it's set, and it isn't `"none"`, or returns false */
export const hasAnyPublicLinkAccess = (item?: Partial<DriveItem>) : Exclude<DriveFileAccessLevel, "none"> | false =>
(item?.access_info?.public?.level && item.access_info.public.level != "none") ? item.access_info.public.level : false;
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import RouterServices from '@features/router/services/router-service';
import useRouterCompany from '@features/router/hooks/use-router-company';
import _ from 'lodash';
import Languages from 'features/global/services/languages-service';
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';

/**
* This will build the context menu in different contexts
Expand All @@ -50,7 +51,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
const { open: preview } = useDrivePreview();
const { viewId } = useRouteState();
const company = useRouterCompany();

function getIdsFromArray(arr: DriveItem[]): string[] {
return arr.map((obj) => obj.id);
}
Expand Down Expand Up @@ -309,9 +310,7 @@ export const useOnBuildContextMenu = (children: DriveItem[], initialParentId?: s
{
type: 'menu',
text: Languages.t('components.item_context_menu.copy_link'),
hide:
!parent?.item?.access_info?.public?.level ||
parent?.item?.access_info?.public?.level === 'none',
hide: !hasAnyPublicLinkAccess(item),
onClick: () => {
copyToClipboard(getPublicLink(item || parent?.item));
ToasterService.success(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useHistory } from 'react-router-dom';
import RouterServices from '@features/router/services/router-service';
import useRouteState from 'app/features/router/hooks/use-route-state';
import { DocumentIcon } from './document-icon';
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';

export const DocumentRow = ({
item,
Expand Down Expand Up @@ -79,7 +80,7 @@ export const DocumentRow = ({
<Base className="flex maxWidth100">{item.name}</Base>
</div>
<div className="shrink-0 ml-4">
{item?.access_info?.public?.level !== 'none' && (
{hasAnyPublicLinkAccess(item) && (
<PublicIcon className="h-5 w-5 text-blue-500" />
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { formatBytes } from '@features/drive/utils';
import { useState } from 'react';
import { PublicIcon } from '../components/public-icon';
import { CheckableIcon, DriveItemProps } from './common';
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';
import './style.scss';

export const FolderRow = ({
Expand Down Expand Up @@ -49,7 +50,7 @@ export const FolderRow = ({
<Base className="!font-semibold flex maxWidth100">{item.name}</Base>
</div>
<div className="shrink-0 ml-4">
{item?.access_info?.public?.level !== 'none' && (
{hasAnyPublicLinkAccess(item) && (
<PublicIcon className="h-5 w-5 text-blue-500" />
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Languages from 'features/global/services/languages-service';
import { useHistory } from 'react-router-dom';
import useRouterCompany from '@features/router/hooks/use-router-company';
import RouterServices from '@features/router/services/router-service';
import { hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';

export default ({
path: livePath,
Expand Down Expand Up @@ -193,7 +194,7 @@ const PathItem = ({
})()}
</Title>
</a>
{item?.access_info?.public?.level && item?.access_info?.public?.level !== 'none' && (
{hasAnyPublicLinkAccess(item) && (
<PublicIcon className="h-5 w-5 ml-2" />
)}
{first && !!user?.id && viewId?.includes('trash') && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ArrowLeftIcon, LockClosedIcon } from '@heroicons/react/outline';
import { PublicLinkAccessOptions } from './public-link-access-options';
import { CuteDepictionOfFolderHierarchy } from './cute-depiction-of-folder-hierarchy';
import { InheritAccessOptions } from './inherit-access-options';
import { changePublicLink, hasAnyPublicLinkAccess } from '@features/files/utils/access-info-helpers';

export type AccessModalType = {
open: boolean;
Expand Down Expand Up @@ -94,20 +95,9 @@ const AccessModalContent = (props: {
refresh(id);
refreshCompany();
}, []);
const havePublicLink = (item?.access_info?.public?.level || 'none') !== 'none';
const havePublicLink = hasAnyPublicLinkAccess(item);
const haveAdvancedSettings = parentItem?.parent_id !== null || havePublicLink;

const updatePublicAccess = (key: string, value: string | number, skipLoading?: true) =>
update({
access_info: {
entities: item?.access_info.entities || [],
public: {
...item!.access_info!.public!,
[key]: value || '',
},
},
}, skipLoading);

return (
<ModalContent
title={
Expand Down Expand Up @@ -138,10 +128,10 @@ const AccessModalContent = (props: {
password={item?.access_info?.public?.password}
expiration={item?.access_info?.public?.expiration}
onChangePassword={(password: string) => {
updatePublicAccess('password', password || '', true);
item && changePublicLink(item, { password: password || '' });
}}
onChangeExpiration={(expiration: number) => {
updatePublicAccess('expiration', expiration || 0);
item && changePublicLink(item, { expiration: expiration || 0 });
}}
/>}
{ parentItem?.parent_id !== null && <>
Expand Down
Loading

0 comments on commit fc3ebaa

Please sign in to comment.